mirror of
https://github.com/home-assistant/core.git
synced 2026-01-06 15:47:19 +01:00
Compare commits
645 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8be2ac70ec | ||
|
|
477ebd99b4 | ||
|
|
093c7f0e44 | ||
|
|
80e9e9bfda | ||
|
|
dafbdbd2d0 | ||
|
|
b641f6863c | ||
|
|
07fcf22aeb | ||
|
|
2d57c6a1c7 | ||
|
|
5c737cfa6e | ||
|
|
e3f682c7d3 | ||
|
|
dbb0525311 | ||
|
|
f641287aa2 | ||
|
|
bbeb64eb24 | ||
|
|
eb2e5e5b9d | ||
|
|
920d298c7e | ||
|
|
93820d5124 | ||
|
|
2e11d49af3 | ||
|
|
2d5ab520ef | ||
|
|
0c14c66fbc | ||
|
|
b1621d4175 | ||
|
|
1860b6c521 | ||
|
|
f59b3da5fe | ||
|
|
e020d5114a | ||
|
|
ce51866bd2 | ||
|
|
931fce8239 | ||
|
|
76d2154820 | ||
|
|
b985e4ef0b | ||
|
|
632256fae2 | ||
|
|
9b43b39370 | ||
|
|
1a635fede3 | ||
|
|
90baa2ce4d | ||
|
|
2f4b2ddc0a | ||
|
|
921760f8c1 | ||
|
|
8ba41563c9 | ||
|
|
a41d0aced7 | ||
|
|
5179832f6f | ||
|
|
ce9bb0e84c | ||
|
|
e5feeec7a4 | ||
|
|
4becfb66e3 | ||
|
|
e4bbbe20dd | ||
|
|
1e758ed030 | ||
|
|
d007269ecc | ||
|
|
bbad15f853 | ||
|
|
de71fee0a0 | ||
|
|
c7a11277ac | ||
|
|
5574686d74 | ||
|
|
15d8f8b827 | ||
|
|
1925748f61 | ||
|
|
226066eafd | ||
|
|
43799b8fee | ||
|
|
9c0171ec5e | ||
|
|
b7141901f6 | ||
|
|
919bb08d02 | ||
|
|
cec39077ba | ||
|
|
9ed4ed2e47 | ||
|
|
d4b05a6a85 | ||
|
|
103377bdb0 | ||
|
|
5fa8037231 | ||
|
|
a2d268a061 | ||
|
|
37f959eb02 | ||
|
|
1ce2b6357a | ||
|
|
527223b992 | ||
|
|
409fd62a7c | ||
|
|
fadd33bcb2 | ||
|
|
904b017552 | ||
|
|
1efa6eaf0f | ||
|
|
9744ec584a | ||
|
|
58dfc1d1b1 | ||
|
|
951af6c76d | ||
|
|
75242e67a7 | ||
|
|
a1208261a8 | ||
|
|
3528705afd | ||
|
|
7d76186798 | ||
|
|
9249b6bc33 | ||
|
|
6cbe28a9cd | ||
|
|
f7b6f8e8fb | ||
|
|
35de3a1dc4 | ||
|
|
815422a886 | ||
|
|
c43a3efabd | ||
|
|
b0ffc55cfa | ||
|
|
cce372ff66 | ||
|
|
5ffda53805 | ||
|
|
60f7a1947f | ||
|
|
0ca80cc27e | ||
|
|
46352f6de9 | ||
|
|
7e3e742938 | ||
|
|
e5756ba41d | ||
|
|
b6ee2332f4 | ||
|
|
c267326891 | ||
|
|
38ad5714cd | ||
|
|
01c7616147 | ||
|
|
fa65783f39 | ||
|
|
9a9342ec3f | ||
|
|
34cb02177d | ||
|
|
5ba4033651 | ||
|
|
5e18c997f7 | ||
|
|
d63028e44a | ||
|
|
f68542ba0d | ||
|
|
9d20a17642 | ||
|
|
e026717239 | ||
|
|
f06cff35ff | ||
|
|
7cb8f49d62 | ||
|
|
edf500e66b | ||
|
|
72a01b8a90 | ||
|
|
3c35d5ea58 | ||
|
|
4d9e681fc1 | ||
|
|
4e388666b2 | ||
|
|
ed012014bc | ||
|
|
bf6c4604f4 | ||
|
|
c91cf66dec | ||
|
|
11125864c6 | ||
|
|
7377ce2640 | ||
|
|
0013139591 | ||
|
|
e3c2d27f4a | ||
|
|
f00d721293 | ||
|
|
b295451d46 | ||
|
|
7a3df037ba | ||
|
|
a60e8b16c0 | ||
|
|
b52cabf2c0 | ||
|
|
cc459e25cc | ||
|
|
d7ca9e7a66 | ||
|
|
f099aee69a | ||
|
|
07bb64815d | ||
|
|
2cfdb44df6 | ||
|
|
2748bc4165 | ||
|
|
f76a4b2806 | ||
|
|
197db6bded | ||
|
|
aa3ccf16ca | ||
|
|
aa91351ff0 | ||
|
|
32da163421 | ||
|
|
ee988dc884 | ||
|
|
05eb73a0e3 | ||
|
|
89e8e1a4c7 | ||
|
|
d081e5ab3a | ||
|
|
ab247b0f4d | ||
|
|
6cd3758b58 | ||
|
|
90e73fda3c | ||
|
|
d5e3cd51a5 | ||
|
|
ecfe0770ed | ||
|
|
6cc5bb0713 | ||
|
|
f6e819e799 | ||
|
|
c42293eb10 | ||
|
|
a6dc86fa75 | ||
|
|
9c386c68dd | ||
|
|
f51d705ac7 | ||
|
|
ba8488d8f1 | ||
|
|
62d0df4f73 | ||
|
|
d675804119 | ||
|
|
eb0a9869d8 | ||
|
|
cd8723f742 | ||
|
|
50cc2ed97c | ||
|
|
dea9aec268 | ||
|
|
5d3fe83e62 | ||
|
|
2277778d8d | ||
|
|
c5d89499fa | ||
|
|
31da54d530 | ||
|
|
5a2ab3167b | ||
|
|
475ac52180 | ||
|
|
660b1b616b | ||
|
|
d8558ad173 | ||
|
|
a93c01788d | ||
|
|
d3c1a48475 | ||
|
|
01672e63ea | ||
|
|
382519e082 | ||
|
|
5d1dbd61b2 | ||
|
|
69dee168a1 | ||
|
|
6d8af58891 | ||
|
|
0bb224d8c7 | ||
|
|
55077b9965 | ||
|
|
ad8ee1383c | ||
|
|
64174f5763 | ||
|
|
df77529bfe | ||
|
|
8cff98d07b | ||
|
|
216c2682f0 | ||
|
|
d952a07658 | ||
|
|
9254e7e862 | ||
|
|
f96e06a2c2 | ||
|
|
3e66df50c8 | ||
|
|
74ac160355 | ||
|
|
c20d48c8e0 | ||
|
|
2ce8c2f80e | ||
|
|
51dc8b78cc | ||
|
|
eb55fc8e77 | ||
|
|
37246449f1 | ||
|
|
2551bf8645 | ||
|
|
749f79e813 | ||
|
|
86568b443c | ||
|
|
29f385ea76 | ||
|
|
289d6b6605 | ||
|
|
73f69085d9 | ||
|
|
118bd34d74 | ||
|
|
f1f033e5d2 | ||
|
|
75a3747f61 | ||
|
|
a5f77d5f46 | ||
|
|
534187f4cd | ||
|
|
8f4fd951e5 | ||
|
|
90a834cbda | ||
|
|
e4e7141ae7 | ||
|
|
c4e1255a84 | ||
|
|
c5574c2684 | ||
|
|
dcbc0b490c | ||
|
|
57a00c1fbf | ||
|
|
aff8c0f695 | ||
|
|
542e430c1c | ||
|
|
26e9e59a5b | ||
|
|
86d265d407 | ||
|
|
23645da74c | ||
|
|
3895979e39 | ||
|
|
5b9d9954c5 | ||
|
|
4c7ec4932c | ||
|
|
06e1c21b1f | ||
|
|
01e581aced | ||
|
|
a107a592de | ||
|
|
134b21dfea | ||
|
|
c27a526f5b | ||
|
|
f4d2ece2fe | ||
|
|
5b8f1850fa | ||
|
|
ce42648a51 | ||
|
|
36e5878b2e | ||
|
|
f0027e3cc1 | ||
|
|
864b57d42c | ||
|
|
b99dd19ad6 | ||
|
|
8806265e99 | ||
|
|
2413d97415 | ||
|
|
395f9b6548 | ||
|
|
7afe694cc7 | ||
|
|
ec2df2ca0f | ||
|
|
65b9383e04 | ||
|
|
ae21fa9ce1 | ||
|
|
564a01f344 | ||
|
|
05bab8c808 | ||
|
|
a0bb554f8a | ||
|
|
2d6b09586d | ||
|
|
573b2a11c0 | ||
|
|
ac25eff2d0 | ||
|
|
05398a9dff | ||
|
|
8c97bccaaa | ||
|
|
5bb201c7fc | ||
|
|
72db4a80dd | ||
|
|
816b1891b5 | ||
|
|
ee8701b560 | ||
|
|
714b516176 | ||
|
|
7b83a836f3 | ||
|
|
ead00e956f | ||
|
|
556dba4020 | ||
|
|
bfe0aee468 | ||
|
|
9de4c2b056 | ||
|
|
c935bfce2a | ||
|
|
7c614a6738 | ||
|
|
d1b519a418 | ||
|
|
e1ed076015 | ||
|
|
63c15e997a | ||
|
|
fb8323f48d | ||
|
|
b5336ed04e | ||
|
|
429367409c | ||
|
|
6dba05c79f | ||
|
|
5c80da6a8f | ||
|
|
d027df5a89 | ||
|
|
b8c1bc9542 | ||
|
|
c53de19246 | ||
|
|
f242ad26ca | ||
|
|
a70af62e60 | ||
|
|
be04ef7be1 | ||
|
|
f4f72e420a | ||
|
|
84287872bb | ||
|
|
78b5eb7aac | ||
|
|
6e44ccf683 | ||
|
|
ad649009cd | ||
|
|
7782e7e948 | ||
|
|
5d5547cdb6 | ||
|
|
22b28d85db | ||
|
|
f5d4f853ba | ||
|
|
0f098df232 | ||
|
|
1f046972d9 | ||
|
|
5a7155fc4a | ||
|
|
c817ab08b7 | ||
|
|
f8005153c9 | ||
|
|
447048701c | ||
|
|
f4e9466394 | ||
|
|
cffc6c7bea | ||
|
|
8d606f8d16 | ||
|
|
7ae814357a | ||
|
|
1be2706de3 | ||
|
|
06d3889e1b | ||
|
|
ee6c9ab6a9 | ||
|
|
82c599a749 | ||
|
|
0b7f873120 | ||
|
|
9f2f0c5566 | ||
|
|
53f8828181 | ||
|
|
22613d8e2e | ||
|
|
efbd66bca1 | ||
|
|
5dfdb9e481 | ||
|
|
6c5989895a | ||
|
|
3acd926d29 | ||
|
|
b6b40286ef | ||
|
|
8a86ec5b74 | ||
|
|
20c5f9de4b | ||
|
|
61730012d8 | ||
|
|
25d2df5689 | ||
|
|
672b83db8a | ||
|
|
b37438ebb7 | ||
|
|
902b72ba1a | ||
|
|
f10fede17f | ||
|
|
c9548b11b1 | ||
|
|
f4aec3ac88 | ||
|
|
e7425e9808 | ||
|
|
978b539111 | ||
|
|
9f4cd5fafe | ||
|
|
ba3c9f9765 | ||
|
|
4ee8be52fe | ||
|
|
866bf887d3 | ||
|
|
dddbce82f5 | ||
|
|
be15ca3f23 | ||
|
|
9a305c9742 | ||
|
|
de231cf9ab | ||
|
|
8325f9db8a | ||
|
|
3f38b9e52f | ||
|
|
1cb2a6add0 | ||
|
|
acf75b5253 | ||
|
|
970bde9e99 | ||
|
|
796143a6c6 | ||
|
|
5569ae38f1 | ||
|
|
7eaad4fb3a | ||
|
|
35c679a956 | ||
|
|
678f273002 | ||
|
|
4e91c65d6e | ||
|
|
f6106706e5 | ||
|
|
ecf337b123 | ||
|
|
f5d8327d9a | ||
|
|
30d4c54187 | ||
|
|
edf20f542a | ||
|
|
9778000e9a | ||
|
|
b5149dfba6 | ||
|
|
ced3cd2616 | ||
|
|
7050236a61 | ||
|
|
c8e1ffad89 | ||
|
|
e3edff8a72 | ||
|
|
d6fd0f405e | ||
|
|
1ab47b5d2b | ||
|
|
a2365eccf6 | ||
|
|
c46ba3446d | ||
|
|
5714f156c3 | ||
|
|
959dd29c90 | ||
|
|
e75a66ed20 | ||
|
|
3e72aa8643 | ||
|
|
5aaa1f8404 | ||
|
|
7292e564f8 | ||
|
|
509cfb6433 | ||
|
|
acdab67c1b | ||
|
|
d7addf59cd | ||
|
|
ccf9edf815 | ||
|
|
2fd3c186e2 | ||
|
|
719199da45 | ||
|
|
a3cd7d653d | ||
|
|
aeeb927e19 | ||
|
|
a3a14f9ea4 | ||
|
|
f4e7b231bc | ||
|
|
5f68735375 | ||
|
|
774fd19638 | ||
|
|
e265401cd0 | ||
|
|
5b3dc7f2a5 | ||
|
|
9ef084d903 | ||
|
|
95b1e257bb | ||
|
|
b06cf87c74 | ||
|
|
326337777a | ||
|
|
2c8a06bfbe | ||
|
|
198a234468 | ||
|
|
e94aa3afe9 | ||
|
|
96e22c7b41 | ||
|
|
33450c726d | ||
|
|
97b9d3bd21 | ||
|
|
fff589eeab | ||
|
|
5e5d2e8ab8 | ||
|
|
1a7ffdca52 | ||
|
|
cada74df22 | ||
|
|
bd3fbe8363 | ||
|
|
4d9c7d9684 | ||
|
|
0bf66384ed | ||
|
|
c7798ef43c | ||
|
|
f4d8095e54 | ||
|
|
5529d77c62 | ||
|
|
c4e151f621 | ||
|
|
f58941a0d4 | ||
|
|
bca673f039 | ||
|
|
2687f2f623 | ||
|
|
134b3d2f3b | ||
|
|
f450c1351c | ||
|
|
c6b10f3703 | ||
|
|
4a08067b9c | ||
|
|
9c37437a59 | ||
|
|
9330142987 | ||
|
|
2bbaac44d4 | ||
|
|
253dee8e4d | ||
|
|
5722cf53bf | ||
|
|
5d301590c3 | ||
|
|
353f5d6b49 | ||
|
|
11da7bed12 | ||
|
|
a358c8e10d | ||
|
|
58826b264a | ||
|
|
56abc7f9b4 | ||
|
|
55d60a6a13 | ||
|
|
5183cb5903 | ||
|
|
0aa8933df6 | ||
|
|
fc46a24996 | ||
|
|
5be58bd056 | ||
|
|
4a423e63f3 | ||
|
|
a8b32edc8e | ||
|
|
4a5b9db394 | ||
|
|
9c23178457 | ||
|
|
764d31efcb | ||
|
|
fc90ccea36 | ||
|
|
aab63ea22a | ||
|
|
157ab77232 | ||
|
|
9a86ccaaea | ||
|
|
32dd815852 | ||
|
|
9ac3928600 | ||
|
|
62e57456e1 | ||
|
|
11f11481b2 | ||
|
|
10f5e9744b | ||
|
|
b2a2193ba3 | ||
|
|
c8b2ba6559 | ||
|
|
1496f1a570 | ||
|
|
493c0bbb4c | ||
|
|
13dd17b2ab | ||
|
|
0ef1c3af34 | ||
|
|
44da43065f | ||
|
|
ffb1613d55 | ||
|
|
b0a2909835 | ||
|
|
b5fb558c62 | ||
|
|
846a0513c7 | ||
|
|
0f1cad24ce | ||
|
|
24630b1ebe | ||
|
|
b705b3ddb9 | ||
|
|
330d352d3a | ||
|
|
bdaae6f844 | ||
|
|
49308bec13 | ||
|
|
edd96c2b04 | ||
|
|
11c4d3892f | ||
|
|
75817ad46d | ||
|
|
780cdd5f90 | ||
|
|
d70eaeb118 | ||
|
|
9e9c6d0184 | ||
|
|
2cdc0febf5 | ||
|
|
c721ec7ab0 | ||
|
|
4e63e8328e | ||
|
|
8f2009a187 | ||
|
|
529eee994b | ||
|
|
3f7dd7ed9a | ||
|
|
b952cfe705 | ||
|
|
b9cf4df557 | ||
|
|
7892297240 | ||
|
|
20fcd1f0e2 | ||
|
|
8bbf13ef9f | ||
|
|
855756cb2a | ||
|
|
bbcfb9158a | ||
|
|
bcd4def0ae | ||
|
|
ddc260b628 | ||
|
|
06bc062221 | ||
|
|
21f3b62d09 | ||
|
|
185ccc4fc4 | ||
|
|
2d337fd29a | ||
|
|
c625e219c7 | ||
|
|
b1736994b7 | ||
|
|
21feff5fd8 | ||
|
|
93118fcade | ||
|
|
5e1c74b430 | ||
|
|
3d3a0a7a4f | ||
|
|
fdd1957750 | ||
|
|
2b97449d98 | ||
|
|
c937a7bcb0 | ||
|
|
e7f442d66b | ||
|
|
2c5d3387f2 | ||
|
|
bb4f23f8e7 | ||
|
|
629b2e81ba | ||
|
|
3508f74fb2 | ||
|
|
d16bc632da | ||
|
|
44d4987536 | ||
|
|
470702261a | ||
|
|
5fb7aa212b | ||
|
|
9522fe3a92 | ||
|
|
ff3c90fb80 | ||
|
|
90ad54da7d | ||
|
|
2baa838ba7 | ||
|
|
a8add06a40 | ||
|
|
1b23b32817 | ||
|
|
eaaa0442e2 | ||
|
|
bc9f2d21c4 | ||
|
|
1a139234af | ||
|
|
d5435cf066 | ||
|
|
46ec6d6dce | ||
|
|
660e777f01 | ||
|
|
de038bae65 | ||
|
|
7774f0ae53 | ||
|
|
7655b6271d | ||
|
|
10bf659773 | ||
|
|
e8a22cb4a8 | ||
|
|
2650c73a89 | ||
|
|
bdf948d866 | ||
|
|
928e025910 | ||
|
|
96aae1292b | ||
|
|
c0bf3d7f32 | ||
|
|
307514e3a7 | ||
|
|
b939626497 | ||
|
|
1522e67351 | ||
|
|
8232f1ef65 | ||
|
|
78f5a8a6f8 | ||
|
|
3044aecbe9 | ||
|
|
a5081ac307 | ||
|
|
f396a4593e | ||
|
|
aac9f972cf | ||
|
|
aaa0944595 | ||
|
|
6cca127bbf | ||
|
|
c1f3ce78e1 | ||
|
|
887b53b794 | ||
|
|
a444df3fde | ||
|
|
b038a1650e | ||
|
|
483556ac5b | ||
|
|
3e9e388745 | ||
|
|
ab42acf4d7 | ||
|
|
0489ae53c4 | ||
|
|
35fcc299c0 | ||
|
|
568c549353 | ||
|
|
edf130b341 | ||
|
|
ed9e93c29f | ||
|
|
55f8ec8866 | ||
|
|
3e70154695 | ||
|
|
aa17481c94 | ||
|
|
b53bc24a63 | ||
|
|
fbd0bf77c7 | ||
|
|
4da2156ebf | ||
|
|
8a67fcfee3 | ||
|
|
08f9793175 | ||
|
|
a5b2fc9759 | ||
|
|
3fa8aff78e | ||
|
|
09ff9cb08e | ||
|
|
c32300a386 | ||
|
|
55dc483c91 | ||
|
|
597ae2e716 | ||
|
|
50887e7e2c | ||
|
|
8743f23f13 | ||
|
|
72fe50bef6 | ||
|
|
bae6333c26 | ||
|
|
a08539d88d | ||
|
|
bf7aecce90 | ||
|
|
e2aa024a05 | ||
|
|
edd5db296d | ||
|
|
e14d6f11c6 | ||
|
|
f3870a8a48 | ||
|
|
31bf5b8ff0 | ||
|
|
a7ae456a06 | ||
|
|
c03022efa3 | ||
|
|
46f5a65e68 | ||
|
|
44ec6b056e | ||
|
|
354007f265 | ||
|
|
6cb8a36cf1 | ||
|
|
435f253be8 | ||
|
|
0fe41ffb00 | ||
|
|
bafa0cc3b8 | ||
|
|
e23aa1ccf8 | ||
|
|
a9db6b16eb | ||
|
|
bf30f2e9e8 | ||
|
|
ee9d50c0a5 | ||
|
|
6736534a52 | ||
|
|
4ccd819ec5 | ||
|
|
52b1e13aca | ||
|
|
67f3910f03 | ||
|
|
64cb3390ea | ||
|
|
0ac4a152be | ||
|
|
4e96e461f7 | ||
|
|
30bed8341a | ||
|
|
d17733427b | ||
|
|
782d2a30cd | ||
|
|
84f30d9ef8 | ||
|
|
ac49298c8d | ||
|
|
a0256e1947 | ||
|
|
7bc2e1238d | ||
|
|
41f558b181 | ||
|
|
383b0914b3 | ||
|
|
be297c4c7e | ||
|
|
aa1f64bed6 | ||
|
|
faf8bbcf13 | ||
|
|
0fa259089d | ||
|
|
f7c7073cd7 | ||
|
|
d7bf3920a5 | ||
|
|
7ee75d67c5 | ||
|
|
d7db3aba36 | ||
|
|
7f99e99dad | ||
|
|
fbea5b4cac | ||
|
|
e6c88c05ad | ||
|
|
757813411d | ||
|
|
7dc05785cc | ||
|
|
d7af43b87d | ||
|
|
6ea74ce740 | ||
|
|
d5bdf7783e | ||
|
|
65d255a626 | ||
|
|
53a735a329 | ||
|
|
31ddcc6278 | ||
|
|
d789de9ea2 | ||
|
|
e2014eb153 | ||
|
|
5932446508 | ||
|
|
61909e873f | ||
|
|
48cf7a4af9 | ||
|
|
9490cb4c8f | ||
|
|
86d4d10176 | ||
|
|
7b3b755aaf | ||
|
|
9afcbaed1d | ||
|
|
3a7cc9bb45 | ||
|
|
ecd1da6525 | ||
|
|
9f2719bb1f | ||
|
|
85d0f2e861 | ||
|
|
5d007e636b | ||
|
|
7cd6f9038c | ||
|
|
a80fd2f243 | ||
|
|
2487d27c45 | ||
|
|
be7162a0df | ||
|
|
c5a8372f13 | ||
|
|
81ca978413 | ||
|
|
d6818c7015 | ||
|
|
df5866dd95 | ||
|
|
34ee2b1ae9 | ||
|
|
8ca897da57 | ||
|
|
c7fcd98cad | ||
|
|
8aa3124aa6 | ||
|
|
b27ba9660b | ||
|
|
9f04b55572 | ||
|
|
c4f4a9a158 | ||
|
|
e2e8b43902 | ||
|
|
3a35642dc1 | ||
|
|
34a7aa2376 | ||
|
|
58eb32bce4 | ||
|
|
c940d26f07 | ||
|
|
fc5e25a07b | ||
|
|
1d32bced1c | ||
|
|
f2a2d6bfa1 | ||
|
|
4f990ce488 | ||
|
|
106b7a9d8f | ||
|
|
ef87d4dad4 | ||
|
|
f6e46aecf5 | ||
|
|
e9cf5f6f42 | ||
|
|
e221c8a37d | ||
|
|
b163544e3c | ||
|
|
a718e92708 | ||
|
|
44d274e428 | ||
|
|
c404fb7142 | ||
|
|
29c7987453 |
63
.coveragerc
63
.coveragerc
@@ -8,15 +8,24 @@ omit =
|
||||
homeassistant/helpers/signal.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
homeassistant/components/apcupsd.py
|
||||
homeassistant/components/*/apcupsd.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
homeassistant/components/android_ip_webcam.py
|
||||
homeassistant/components/*/android_ip_webcam.py
|
||||
|
||||
homeassistant/components/bbb_gpio.py
|
||||
homeassistant/components/*/bbb_gpio.py
|
||||
|
||||
homeassistant/components/blink.py
|
||||
homeassistant/components/*/blink.py
|
||||
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
@@ -53,6 +62,9 @@ omit =
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
homeassistant/components/lutron_caseta.py
|
||||
homeassistant/components/*/lutron_caseta.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
@@ -85,6 +97,13 @@ omit =
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/tradfri.py
|
||||
homeassistant/components/*/tradfri.py
|
||||
|
||||
homeassistant/components/twilio.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
homeassistant/components/vera.py
|
||||
homeassistant/components/*/vera.py
|
||||
|
||||
@@ -105,9 +124,6 @@ omit =
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zwave/*
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
@@ -132,16 +148,24 @@ omit =
|
||||
homeassistant/components/zabbix.py
|
||||
homeassistant/components/*/zabbix.py
|
||||
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||
homeassistant/components/apiai.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
homeassistant/components/binary_sensor/flic.py
|
||||
homeassistant/components/binary_sensor/hikvision.py
|
||||
homeassistant/components/binary_sensor/iss.py
|
||||
homeassistant/components/binary_sensor/ping.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
@@ -207,17 +231,22 @@ omit =
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx/*.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/tradfri.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/apple_tv.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
@@ -231,6 +260,7 @@ omit =
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.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/hdmi_cec.py
|
||||
@@ -253,12 +283,15 @@ omit =
|
||||
homeassistant/components/media_player/samsungtv.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
@@ -286,14 +319,13 @@ omit =
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/telstra.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/amcrest.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
@@ -303,16 +335,19 @@ omit =
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/crimereports.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
@@ -333,12 +368,16 @@ omit =
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/mhz19.py
|
||||
homeassistant/components/sensor/lyft.py
|
||||
homeassistant/components/sensor/metoffice.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/mvglive.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
@@ -347,6 +386,7 @@ omit =
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openevse.py
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/opensky.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
@@ -404,14 +444,17 @@ omit =
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/telegram_webhooks.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/metoffice.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/util.py
|
||||
|
||||
|
||||
[report]
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,16 +1,16 @@
|
||||
**Description:**
|
||||
## Description:
|
||||
|
||||
|
||||
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
|
||||
|
||||
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
|
||||
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
## Example entry for `configuration.yaml` (if applicable):
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
## Checklist:
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,15 +1,4 @@
|
||||
config/*
|
||||
!config/home-assistant.conf.default
|
||||
|
||||
# There is not a better solution afaik..
|
||||
!config/custom_components
|
||||
config/custom_components/*
|
||||
!config/custom_components/example.py
|
||||
!config/custom_components/hello_world.py
|
||||
!config/custom_components/mqtt_example.py
|
||||
!config/panels
|
||||
config/panels/*
|
||||
!config/panels/react.html
|
||||
|
||||
tests/testing_config/deps
|
||||
tests/testing_config/home-assistant.log
|
||||
|
||||
@@ -14,6 +14,8 @@ matrix:
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.6-dev"
|
||||
env: TOXENV=py36
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
|
||||
@@ -8,6 +8,7 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
#ENV INSTALL_OPENZWAVE no
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_COAP_CLIENT no
|
||||
|
||||
VOLUME /config
|
||||
|
||||
@@ -21,7 +22,7 @@ RUN virtualization/Docker/setup_docker_prereqs
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
include README.rst
|
||||
include LICENSE
|
||||
include LICENSE.md
|
||||
graft homeassistant
|
||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
83
README.rst
83
README.rst
@@ -1,9 +1,7 @@
|
||||
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs|
|
||||
==============================================================================================================================================================================================
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. The
|
||||
goal of Home Assistant is to be able to track and control all devices at
|
||||
home and offer a platform for automating control.
|
||||
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
|
||||
|
||||
To get started:
|
||||
|
||||
@@ -12,83 +10,22 @@ To get started:
|
||||
python3 -m pip install homeassistant
|
||||
hass --open-ui
|
||||
|
||||
Check out `the website <https://home-assistant.io>`__ for `a
|
||||
demo <https://home-assistant.io/demo/>`__, installation instructions,
|
||||
tutorials and documentation.
|
||||
Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
||||
demo <https://home-assistant.io/demo/>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
||||
`tutorials <https://home-assistant.io/getting-started/automation-2/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
||||
|
||||
|screenshot-states|
|
||||
|
||||
Examples of devices Home Assistant can interface with:
|
||||
Featured integrations
|
||||
---------------------
|
||||
|
||||
- Monitoring connected devices to a wireless router:
|
||||
`OpenWrt <https://openwrt.org/>`__,
|
||||
`Tomato <http://www.polarcloud.com/tomato>`__,
|
||||
`Netgear <http://netgear.com>`__,
|
||||
`DD-WRT <http://www.dd-wrt.com/site/index>`__,
|
||||
`TPLink <http://www.tp-link.us/>`__,
|
||||
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__,
|
||||
`Xiaomi <http://miwifi.com/>`__ and any SNMP
|
||||
capable Linksys WAP/WRT
|
||||
- `Philips Hue <http://meethue.com>`__ lights,
|
||||
`WeMo <http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/>`__
|
||||
switches, `Edimax <http://www.edimax.com/>`__ switches,
|
||||
`Efergy <https://efergy.com>`__ energy monitoring, and
|
||||
`Tellstick <http://www.telldus.se/products/tellstick>`__ devices and
|
||||
sensors
|
||||
- `Google
|
||||
Chromecasts <http://www.google.com/intl/en/chrome/devices/chromecast>`__,
|
||||
`Music Player Daemon <http://www.musicpd.org/>`__, `Logitech
|
||||
Squeezebox <https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29>`__,
|
||||
`Plex <https://plex.tv/>`__, `Kodi (XBMC) <http://kodi.tv/>`__,
|
||||
iTunes (by way of
|
||||
`itunes-api <https://github.com/maddox/itunes-api>`__), and Amazon
|
||||
Fire TV (by way of
|
||||
`python-firetv <https://github.com/happyleavesaoc/python-firetv>`__)
|
||||
- Support for
|
||||
`ISY994 <https://www.universal-devices.com/residential/isy994i-series/>`__
|
||||
(Insteon and X10 devices), `Z-Wave <http://www.z-wave.com/>`__, `Nest
|
||||
Thermostats <https://nest.com/>`__,
|
||||
`RFXtrx <http://www.rfxcom.com/>`__,
|
||||
`Arduino <https://www.arduino.cc/>`__, `Raspberry
|
||||
Pi <https://www.raspberrypi.org/>`__, and
|
||||
`Modbus <http://www.modbus.org/>`__
|
||||
- Interaction with `IFTTT <https://ifttt.com/>`__
|
||||
- Integrate data from the `Bitcoin <https://bitcoin.org>`__ network,
|
||||
meteorological data from
|
||||
`OpenWeatherMap <http://openweathermap.org/>`__ and
|
||||
`Forecast.io <https://forecast.io/>`__,
|
||||
`Transmission <http://www.transmissionbt.com/>`__, or
|
||||
`SABnzbd <http://sabnzbd.org>`__.
|
||||
- `See full list of supported
|
||||
devices <https://home-assistant.io/components/>`__
|
||||
|screenshot-components|
|
||||
|
||||
Build home automation on top of your devices:
|
||||
|
||||
- Keep a precise history of every change to the state of your house
|
||||
- Turn on the lights when people get home after sunset
|
||||
- Turn on lights slowly during sunset to compensate for less light
|
||||
- Turn off all lights and devices when everybody leaves the house
|
||||
- Offers a `REST API <https://home-assistant.io/developers/rest_api/>`__
|
||||
and can interface with MQTT for easy integration with other projects
|
||||
like `OwnTracks <http://owntracks.org/>`__
|
||||
- Allow sending notifications using
|
||||
`Instapush <https://instapush.im>`__, `Notify My Android
|
||||
(NMA) <http://www.notifymyandroid.com/>`__,
|
||||
`PushBullet <https://www.pushbullet.com/>`__,
|
||||
`PushOver <https://pushover.net/>`__,
|
||||
`Slack <https://slack.com/>`__,
|
||||
`Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber
|
||||
(XMPP) <http://xmpp.org>`__
|
||||
|
||||
The system is built using a modular approach so support for other devices or actions can
|
||||
be implemented easily. See also the `section on
|
||||
architecture <https://home-assistant.io/developers/architecture/>`__
|
||||
and the `section on creating your own
|
||||
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://home-assistant.io/developers/architecture/>`__ and the `section on creating your own
|
||||
components <https://home-assistant.io/developers/creating_components/>`__.
|
||||
|
||||
If you run into issues while using Home Assistant or during development
|
||||
of a component, check the `Home Assistant help
|
||||
section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.
|
||||
|
||||
.. |Build Status| image:: https://travis-ci.org/home-assistant/home-assistant.svg?branch=master
|
||||
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||
@@ -100,3 +37,5 @@ section <https://home-assistant.io/help/>`__ of our website for further help and
|
||||
:target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
:target: https://home-assistant.io/demo/
|
||||
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png
|
||||
:target: https://home-assistant.io/components/
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
homeassistant:
|
||||
# Omitted values in this section will be auto detected using freegeoip.io
|
||||
|
||||
# Location required to calculate the time the sun rises and sets.
|
||||
# Coordinates are also used for location for weather related components.
|
||||
# Google Maps can be used to determine more precise GPS coordinates.
|
||||
latitude: 32.87336
|
||||
longitude: 117.22743
|
||||
|
||||
# Impacts weather/sunrise data
|
||||
elevation: 665
|
||||
|
||||
# 'metric' for Metric System, 'imperial' for imperial system
|
||||
unit_system: metric
|
||||
|
||||
# Pick yours from here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
time_zone: America/Los_Angeles
|
||||
|
||||
# Name of the location where Home Assistant is running
|
||||
name: Home
|
||||
|
||||
http:
|
||||
api_password: mypass
|
||||
# Set to 1 to enable development mode
|
||||
# development: 1
|
||||
|
||||
# Enable the frontend
|
||||
frontend:
|
||||
|
||||
light:
|
||||
# platform: hue
|
||||
|
||||
wink:
|
||||
# Get your token at https://winkbearertoken.appspot.com
|
||||
access_token: 'YOUR_TOKEN'
|
||||
|
||||
device_tracker:
|
||||
# The following tracker are available:
|
||||
# https://home-assistant.io/components/#presence-detection
|
||||
platform: netgear
|
||||
host: 192.168.1.1
|
||||
username: admin
|
||||
password: PASSWORD
|
||||
|
||||
switch:
|
||||
platform: wemo
|
||||
|
||||
climate:
|
||||
platform: nest
|
||||
# Required: username and password that are used to login to the Nest thermostat.
|
||||
username: myemail@mydomain.com
|
||||
password: mypassword
|
||||
|
||||
downloader:
|
||||
download_dir: downloads
|
||||
|
||||
notify:
|
||||
platform: pushbullet
|
||||
api_key: ABCDEFGHJKLMNOPQRSTUVXYZ
|
||||
|
||||
device_sun_light_trigger:
|
||||
# Optional: specify a specific light/group of lights that has to be turned on
|
||||
light_group: group.living_room
|
||||
# Optional: specify which light profile to use when turning lights on
|
||||
light_profile: relax
|
||||
# Optional: disable lights being turned off when everybody leaves the house
|
||||
# disable_turn_off: 1
|
||||
|
||||
# A comma separated list of states that have to be tracked as a single group
|
||||
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
|
||||
# You can also have groups within groups.
|
||||
# https://home-assistant.io/components/group/
|
||||
group:
|
||||
default_view:
|
||||
view: yes
|
||||
entities:
|
||||
- group.awesome_people
|
||||
- group.climate
|
||||
kitchen:
|
||||
name: Kitchen
|
||||
entities:
|
||||
- switch.kitchen_pin_3
|
||||
upstairs:
|
||||
name: Kids
|
||||
icon: mdi:account-multiple
|
||||
view: yes
|
||||
entities:
|
||||
- input_boolean.notify_home
|
||||
- camera.demo_camera
|
||||
|
||||
browser:
|
||||
keyboard:
|
||||
|
||||
# https://home-assistant.io/getting-started/automation/
|
||||
automation:
|
||||
- alias: Turn on light when sun sets
|
||||
trigger:
|
||||
platform: sun
|
||||
event: sunset
|
||||
offset: "-01:00:00"
|
||||
condition:
|
||||
condition: state
|
||||
entity_id: group.all_devices
|
||||
state: 'home'
|
||||
action:
|
||||
service: light.turn_on
|
||||
|
||||
# Another way to do is to collect all entries under one "sensor:"
|
||||
# sensor:
|
||||
# - platform: mqtt
|
||||
# name: "MQTT Sensor 1"
|
||||
# - platform: mqtt
|
||||
# name: "MQTT Sensor 2"
|
||||
#
|
||||
# Details: https://home-assistant.io/getting-started/devices/
|
||||
|
||||
sensor:
|
||||
platform: systemmonitor
|
||||
resources:
|
||||
- type: 'disk_use_percent'
|
||||
arg: '/'
|
||||
- type: 'disk_use_percent'
|
||||
arg: '/home'
|
||||
|
||||
sensor 2:
|
||||
platform: cpuspeed
|
||||
|
||||
script:
|
||||
wakeup:
|
||||
alias: Wake Up
|
||||
sequence:
|
||||
- event: LOGBOOK_ENTRY
|
||||
event_data:
|
||||
name: Paulus
|
||||
message: is waking up
|
||||
entity_id: device_tracker.paulus
|
||||
domain: light
|
||||
- alias: Bedroom lights on
|
||||
service: light.turn_on
|
||||
data:
|
||||
entity_id: group.bedroom
|
||||
brightness: 100
|
||||
- delay:
|
||||
minutes: 1
|
||||
- alias: Living room lights on
|
||||
service: light.turn_on
|
||||
data:
|
||||
entity_id: group.living_room
|
||||
|
||||
scene:
|
||||
- name: Romantic
|
||||
entities:
|
||||
light.tv_back_light: on
|
||||
light.ceiling:
|
||||
state: on
|
||||
xy_color: [0.33, 0.66]
|
||||
brightness: 200
|
||||
@@ -1,149 +0,0 @@
|
||||
"""
|
||||
Example of a custom component.
|
||||
|
||||
Example component to target an entity_id to:
|
||||
- turn it on at 7AM in the morning
|
||||
- turn it on if anyone comes home and it is off
|
||||
- turn it off if all lights are turned off
|
||||
- turn it off if all people leave the house
|
||||
- offer a service to turn it on for 10 seconds
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Example custom component you will need to add the following to
|
||||
your configuration.yaml file.
|
||||
|
||||
example:
|
||||
target: TARGET_ENTITY
|
||||
|
||||
Variable:
|
||||
|
||||
target
|
||||
*Required
|
||||
TARGET_ENTITY should be one of your devices that can be turned on and off,
|
||||
ie a light or a switch. Example value could be light.Ceiling or switch.AC
|
||||
(if you have these devices with those names).
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.event_decorators import \
|
||||
track_state_change, track_time_change
|
||||
from homeassistant.helpers.service import service
|
||||
import homeassistant.components as core
|
||||
from homeassistant.components import device_tracker
|
||||
from homeassistant.components import light
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component.
|
||||
DOMAIN = "example"
|
||||
|
||||
# List of component names (string) your component depends upon.
|
||||
# We depend on group because group will be loaded after all the components that
|
||||
# initialize devices have been setup.
|
||||
DEPENDENCIES = ['group', 'device_tracker', 'light']
|
||||
|
||||
# Configuration key for the entity id we are targeting.
|
||||
CONF_TARGET = 'target'
|
||||
|
||||
# Variable for storing configuration parameters.
|
||||
TARGET_ID = None
|
||||
|
||||
# Name of the service that we expose.
|
||||
SERVICE_FLASH = 'flash'
|
||||
|
||||
# Shortcut for the logger
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup example component."""
|
||||
global TARGET_ID
|
||||
|
||||
# Validate that all required config options are given.
|
||||
if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER):
|
||||
return False
|
||||
|
||||
TARGET_ID = config[DOMAIN][CONF_TARGET]
|
||||
|
||||
# Validate that the target entity id exists.
|
||||
if hass.states.get(TARGET_ID) is None:
|
||||
_LOGGER.error("Target entity id %s does not exist",
|
||||
TARGET_ID)
|
||||
|
||||
# Tell the bootstrapper that we failed to initialize and clear the
|
||||
# stored target id so our functions don't run.
|
||||
TARGET_ID = None
|
||||
return False
|
||||
|
||||
# Tell the bootstrapper that we initialized successfully.
|
||||
return True
|
||||
|
||||
|
||||
@track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES)
|
||||
def track_devices(hass, entity_id, old_state, new_state):
|
||||
"""Called when the group.all devices change state."""
|
||||
# If the target id is not set, return
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
# If anyone comes home and the entity is not on, turn it on.
|
||||
if new_state.state == STATE_HOME and not core.is_on(hass, TARGET_ID):
|
||||
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
# If all people leave the house and the entity is on, turn it off.
|
||||
elif new_state.state == STATE_NOT_HOME and core.is_on(hass, TARGET_ID):
|
||||
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
|
||||
@track_time_change(hour=7, minute=0, second=0)
|
||||
def wake_up(hass, now):
|
||||
"""Turn light on in the morning.
|
||||
|
||||
Turn the light on at 7 AM if there are people home and it is not already
|
||||
on.
|
||||
"""
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
if device_tracker.is_on(hass) and not core.is_on(hass, TARGET_ID):
|
||||
_LOGGER.info('People home at 7AM, turning it on')
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
|
||||
@track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF)
|
||||
def all_lights_off(hass, entity_id, old_state, new_state):
|
||||
"""If all lights turn off, turn off."""
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
if core.is_on(hass, TARGET_ID):
|
||||
_LOGGER.info('All lights have been turned off, turning it off')
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
|
||||
@service(DOMAIN, SERVICE_FLASH)
|
||||
def flash_service(hass, call):
|
||||
"""Service that will toggle the target.
|
||||
|
||||
Set the light to off for 10 seconds if on and vice versa.
|
||||
"""
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
if core.is_on(hass, TARGET_ID):
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
else:
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
@@ -1,27 +0,0 @@
|
||||
"""
|
||||
The "hello world" custom component.
|
||||
|
||||
This component implements the bare minimum that a component should implement.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the hello_word component you will need to add the following to your
|
||||
configuration.yaml file.
|
||||
|
||||
hello_world:
|
||||
"""
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component.
|
||||
DOMAIN = "hello_world"
|
||||
|
||||
# List of component names (string) your component depends upon.
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup our skeleton component."""
|
||||
# States are in the format DOMAIN.OBJECT_ID.
|
||||
hass.states.set('hello_world.Hello_World', 'Works!')
|
||||
|
||||
# Return boolean to indicate that initialization was successfully.
|
||||
return True
|
||||
@@ -1,55 +0,0 @@
|
||||
"""
|
||||
Example of a custom MQTT component.
|
||||
|
||||
Shows how to communicate with MQTT. Follows a topic on MQTT and updates the
|
||||
state of an entity to the last message received on that topic.
|
||||
|
||||
Also offers a service 'set_state' that will publish a message on the topic that
|
||||
will be passed via MQTT to our message received listener. Call the service with
|
||||
example payload {"new_state": "some new state"}.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the mqtt_example component you will need to add the following to your
|
||||
configuration.yaml file.
|
||||
|
||||
mqtt_example:
|
||||
topic: "home-assistant/mqtt_example"
|
||||
"""
|
||||
import homeassistant.loader as loader
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component.
|
||||
DOMAIN = "mqtt_example"
|
||||
|
||||
# List of component names (string) your component depends upon.
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_TOPIC = 'topic'
|
||||
DEFAULT_TOPIC = 'home-assistant/mqtt_example'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the MQTT example component."""
|
||||
mqtt = loader.get_component('mqtt')
|
||||
topic = config[DOMAIN].get('topic', DEFAULT_TOPIC)
|
||||
entity_id = 'mqtt_example.last_message'
|
||||
|
||||
# Listen to a message on MQTT.
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
hass.states.set(entity_id, payload)
|
||||
|
||||
mqtt.subscribe(hass, topic, message_received)
|
||||
|
||||
hass.states.set(entity_id, 'No messages')
|
||||
|
||||
# Service to publish a message on MQTT.
|
||||
def set_state_service(call):
|
||||
"""Service to send a message."""
|
||||
mqtt.publish(hass, topic, call.data.get('new_state'))
|
||||
|
||||
# Register our service with Home Assistant.
|
||||
hass.services.register(DOMAIN, 'set_state', set_state_service)
|
||||
|
||||
# Return boolean to indicate that initialization was successfully.
|
||||
return True
|
||||
@@ -1,432 +0,0 @@
|
||||
<!--
|
||||
Custom Home Assistant panel example.
|
||||
|
||||
Currently only works in Firefox and Chrome because it uses ES6.
|
||||
|
||||
Make sure this file is in <config>/panels/react.html
|
||||
|
||||
Add to your configuration.yaml:
|
||||
|
||||
panel_custom:
|
||||
- name: react
|
||||
sidebar_title: TodoMVC
|
||||
sidebar_icon: mdi:checkbox-marked-outline
|
||||
config:
|
||||
title: Wow hello!
|
||||
-->
|
||||
|
||||
<script src="https://fb.me/react-15.2.1.min.js"></script>
|
||||
<script src="https://fb.me/react-dom-15.2.1.min.js"></script>
|
||||
|
||||
<!-- for development, replace with:
|
||||
<script src="https://fb.me/react-15.2.1.js"></script>
|
||||
<script src="https://fb.me/react-dom-15.2.1.js"></script>
|
||||
-->
|
||||
|
||||
<!--
|
||||
CSS taken from ReactJS TodoMVC example by Pete Hunt
|
||||
http://todomvc.com/examples/react/
|
||||
-->
|
||||
|
||||
<style>
|
||||
.todoapp input[type="checkbox"] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.todoapp {
|
||||
background: #fff;
|
||||
margin: 130px 0 40px 0;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
|
||||
0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.todoapp h1 {
|
||||
position: absolute;
|
||||
top: -155px;
|
||||
width: 100%;
|
||||
font-size: 100px;
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
color: rgba(175, 47, 47, 0.15);
|
||||
-webkit-text-rendering: optimizeLegibility;
|
||||
-moz-text-rendering: optimizeLegibility;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.todoapp .main {
|
||||
position: relative;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp .todo-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.todoapp .todo-list li {
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.todoapp .todo-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.todoapp .todo-list li .toggle {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
border: none; /* Mobile Safari */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.todoapp .todo-list li .toggle:focus {
|
||||
border-left: 3px solid rgba(175, 47, 47, 0.35);
|
||||
}
|
||||
|
||||
.todoapp .todo-list li .toggle:after {
|
||||
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
|
||||
}
|
||||
|
||||
.todoapp .todo-list li .toggle:checked:after {
|
||||
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
|
||||
}
|
||||
|
||||
.todoapp .todo-list li label {
|
||||
white-space: pre-line;
|
||||
word-break: break-all;
|
||||
padding: 15px 60px 15px 15px;
|
||||
margin-left: 45px;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
transition: color 0.4s;
|
||||
}
|
||||
|
||||
.todoapp .todo-list li.completed label {
|
||||
color: #d9d9d9;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todoapp .footer {
|
||||
color: #777;
|
||||
padding: 10px 15px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.todoapp .footer:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
|
||||
0 8px 0 -3px #f6f6f6,
|
||||
0 9px 1px -3px rgba(0, 0, 0, 0.2),
|
||||
0 16px 0 -6px #f6f6f6,
|
||||
0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.todoapp .todo-count {
|
||||
float: left;
|
||||
text-align: left;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.todoapp .toggle-menu {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
font-weight: 300;
|
||||
color: rgba(175, 47, 47, 0.75);
|
||||
}
|
||||
|
||||
.todoapp .filters {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.todoapp .filters li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.todoapp .filters li a {
|
||||
color: inherit;
|
||||
margin: 3px;
|
||||
padding: 3px 7px;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.todoapp .filters li a.selected,
|
||||
.filters li a:hover {
|
||||
border-color: rgba(175, 47, 47, 0.1);
|
||||
}
|
||||
|
||||
.todoapp .filters li a.selected {
|
||||
border-color: rgba(175, 47, 47, 0.2);
|
||||
}
|
||||
|
||||
/*
|
||||
Hack to remove background from Mobile Safari.
|
||||
Can't use it globally since it destroys checkboxes in Firefox
|
||||
*/
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
.todoapp .toggle-all,
|
||||
.todoapp .todo-list li .toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.todoapp .todo-list li .toggle {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.todoapp .toggle-all {
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.todoapp .footer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.todoapp .filters {
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<dom-module id='ha-panel-react'>
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
background: #f5f5f5;
|
||||
display: block;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.mount {
|
||||
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4em;
|
||||
color: #4d4d4d;
|
||||
min-width: 230px;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-font-smoothing: antialiased;
|
||||
font-smoothing: antialiased;
|
||||
font-weight: 300;
|
||||
}
|
||||
</style>
|
||||
<div id='mount' class='mount'></div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
// Example uses ES6. Will only work in modern browsers
|
||||
class TodoMVC extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: 'all',
|
||||
// load initial value of entities
|
||||
entities: this.props.hass.reactor.evaluate(
|
||||
this.props.hass.entityGetters.visibleEntityMap),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// register to entity updates
|
||||
this._unwatchHass = this.props.hass.reactor.observe(
|
||||
this.props.hass.entityGetters.visibleEntityMap,
|
||||
entities => this.setState({entities}))
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// unregister to entity updates
|
||||
this._unwatchHass();
|
||||
}
|
||||
|
||||
handlePickFilter(filter, ev) {
|
||||
ev.preventDefault();
|
||||
this.setState({filter});
|
||||
}
|
||||
|
||||
handleEntityToggle(entity, ev) {
|
||||
this.props.hass.serviceActions.callService(
|
||||
entity.domain, 'toggle', { entity_id: entity.entityId });
|
||||
}
|
||||
|
||||
handleToggleMenu(ev) {
|
||||
ev.preventDefault();
|
||||
Polymer.Base.fire('open-menu', null, {node: ev.target});
|
||||
}
|
||||
|
||||
entityRow(entity) {
|
||||
const completed = entity.state === 'on';
|
||||
|
||||
return React.createElement(
|
||||
'li', {
|
||||
className: completed && 'completed',
|
||||
key: entity.entityId,
|
||||
},
|
||||
React.createElement(
|
||||
"div", { className: "view" },
|
||||
React.createElement(
|
||||
"input", {
|
||||
checked: completed,
|
||||
className: "toggle",
|
||||
type: "checkbox",
|
||||
onChange: ev => this.handleEntityToggle(entity, ev),
|
||||
}),
|
||||
React.createElement("label", null, entity.entityDisplay)));
|
||||
}
|
||||
|
||||
filterRow(filter) {
|
||||
return React.createElement(
|
||||
"li", { key: filter },
|
||||
React.createElement(
|
||||
"a", {
|
||||
href: "#",
|
||||
className: this.state.filter === filter && "selected",
|
||||
onClick: ev => this.handlePickFilter(filter, ev),
|
||||
},
|
||||
filter.substring(0, 1).toUpperCase() + filter.substring(1)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { entities, filter } = this.state;
|
||||
|
||||
if (!entities) return null;
|
||||
|
||||
const filters = ['all', 'light', 'switch'];
|
||||
|
||||
const showEntities = filter === 'all' ?
|
||||
entities.filter(ent => filters.includes(ent.domain)) :
|
||||
entities.filter(ent => ent.domain == filter);
|
||||
|
||||
return React.createElement(
|
||||
'div', { className: 'todoapp-wrapper' },
|
||||
React.createElement(
|
||||
"section", { className: "todoapp" },
|
||||
React.createElement(
|
||||
"div", null,
|
||||
React.createElement(
|
||||
"header", { className: "header" },
|
||||
React.createElement("h1", null, this.props.title || "todos")
|
||||
),
|
||||
React.createElement(
|
||||
"section", { className: "main" },
|
||||
React.createElement(
|
||||
"ul", { className: "todo-list" },
|
||||
showEntities.valueSeq().map(ent => this.entityRow(ent)))
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
"footer", { className: "footer" },
|
||||
React.createElement(
|
||||
"span", { className: "todo-count" },
|
||||
showEntities.filter(ent => ent.state === 'off').size + " items left"
|
||||
),
|
||||
React.createElement(
|
||||
"ul", { className: "filters" },
|
||||
filters.map(filter => this.filterRow(filter))
|
||||
),
|
||||
!this.props.showMenu && React.createElement(
|
||||
"a", {
|
||||
className: "toggle-menu",
|
||||
href: '#',
|
||||
onClick: ev => this.handleToggleMenu(ev),
|
||||
},
|
||||
"Show menu"
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Polymer({
|
||||
is: 'ha-panel-react',
|
||||
|
||||
properties: {
|
||||
// Home Assistant object
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
// If should render in narrow mode
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
// If sidebar is currently shown
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
// Home Assistant panel info
|
||||
// panel.config contains config passed to register_panel serverside
|
||||
panel: {
|
||||
type: Object,
|
||||
}
|
||||
},
|
||||
|
||||
// This will make sure we forward changed properties to React
|
||||
observers: [
|
||||
'propsChanged(hass, narrow, showMenu, panel)',
|
||||
],
|
||||
|
||||
// Mount React when element attached
|
||||
attached: function () {
|
||||
this.mount(this.hass, this.narrow, this.showMenu, this.panel);
|
||||
},
|
||||
|
||||
// Called when properties change
|
||||
propsChanged: function (hass, narrow, showMenu, panel) {
|
||||
this.mount(hass, narrow, showMenu, panel);
|
||||
},
|
||||
|
||||
// Render React. Debounce in case multiple properties change.
|
||||
mount: function (hass, narrow, showMenu, panel) {
|
||||
this.debounce('mount', function () {
|
||||
ReactDOM.render(React.createElement(TodoMVC, {
|
||||
hass: hass,
|
||||
narrow: narrow,
|
||||
showMenu: showMenu,
|
||||
title: panel.config ? panel.config.title : null
|
||||
}), this.$.mount);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
// Unmount React node when panel no longer in use.
|
||||
detached: function () {
|
||||
ReactDOM.unmountComponentAtNode(this.$.mount);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
BIN
docs/screenshot-components.png
Executable file
BIN
docs/screenshot-components.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -20,6 +20,17 @@ from homeassistant.const import (
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
|
||||
def attempt_use_uvloop():
|
||||
"""Attempt to use uvloop."""
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
import uvloop
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def monkey_patch_asyncio():
|
||||
"""Replace weakref.WeakSet to address Python 3 bug.
|
||||
|
||||
@@ -255,10 +266,13 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
||||
|
||||
def cmdline() -> List[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if sys.argv[0].endswith('/__main__.py'):
|
||||
if sys.argv[0].endswith(os.path.sep + '__main__.py'):
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
arg != '--daemon']
|
||||
else:
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
@@ -308,8 +322,7 @@ def setup_and_run_hass(config_dir: str,
|
||||
EVENT_HOMEASSISTANT_START, open_browser
|
||||
)
|
||||
|
||||
hass.start()
|
||||
return hass.exit_code
|
||||
return hass.start()
|
||||
|
||||
|
||||
def try_to_restart() -> None:
|
||||
@@ -356,11 +369,13 @@ def try_to_restart() -> None:
|
||||
|
||||
def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
attempt_use_uvloop()
|
||||
|
||||
if sys.version_info[:3] < (3, 5, 3):
|
||||
monkey_patch_asyncio()
|
||||
|
||||
validate_python()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
if args.script is not None:
|
||||
|
||||
@@ -4,320 +4,31 @@ import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from time import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.util.async import (
|
||||
run_coroutine_threadsafe, run_callback_threadsafe)
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
_PERSISTENT_ERRORS = {}
|
||||
HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)'
|
||||
|
||||
|
||||
def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_setup_component(hass, domain, config), loop=hass.loop).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if domain in hass.config.components:
|
||||
_LOGGER.debug('Component %s already set up.', domain)
|
||||
return True
|
||||
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
if config is None:
|
||||
config = {}
|
||||
|
||||
components = loader.load_order_component(domain)
|
||||
|
||||
# OrderedSet is empty if component or dependencies could not be resolved
|
||||
if not components:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
for component in components:
|
||||
res = yield from _async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error('Component %s failed to setup', component)
|
||||
_async_persistent_notification(hass, component, True)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_requirements(hass: core.HomeAssistant, component,
|
||||
name: str) -> bool:
|
||||
"""Install the requirements for a component.
|
||||
|
||||
This method needs to run in an executor.
|
||||
"""
|
||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||
return True
|
||||
|
||||
for req in component.REQUIREMENTS:
|
||||
if not pkg_util.install_package(req, target=hass.config.path('deps')):
|
||||
_LOGGER.error('Not initializing %s because could not install '
|
||||
'dependency %s', name, req)
|
||||
_async_persistent_notification(hass, name)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_setup_component(hass: core.HomeAssistant,
|
||||
domain: str, config) -> bool:
|
||||
"""Setup a component for Home Assistant.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
setup_progress = hass.data.get('setup_progress')
|
||||
if setup_progress is None:
|
||||
setup_progress = hass.data['setup_progress'] = []
|
||||
|
||||
if domain in setup_progress:
|
||||
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
||||
domain, domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Used to indicate to discovery that a setup is ongoing and allow it
|
||||
# to wait till it is done.
|
||||
did_lock = False
|
||||
if not setup_lock.locked():
|
||||
yield from setup_lock.acquire()
|
||||
did_lock = True
|
||||
|
||||
setup_progress.append(domain)
|
||||
config = yield from async_prepare_setup_component(hass, config, domain)
|
||||
|
||||
if config is None:
|
||||
return False
|
||||
|
||||
component = loader.get_component(domain)
|
||||
if component is None:
|
||||
_async_persistent_notification(hass, domain)
|
||||
return False
|
||||
|
||||
async_comp = hasattr(component, 'async_setup')
|
||||
|
||||
try:
|
||||
_LOGGER.info("Setting up %s", domain)
|
||||
if async_comp:
|
||||
result = yield from component.async_setup(hass, config)
|
||||
else:
|
||||
result = yield from hass.loop.run_in_executor(
|
||||
None, component.setup, hass, config)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error during setup of component %s', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
if result is False:
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
elif result is not True:
|
||||
_LOGGER.error('component %s did not return boolean if setup '
|
||||
'was successful. Disabling component.', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
loader.set_component(domain, None)
|
||||
return False
|
||||
|
||||
hass.config.components.add(component.DOMAIN)
|
||||
|
||||
hass.bus.async_fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||
)
|
||||
|
||||
return True
|
||||
finally:
|
||||
setup_progress.remove(domain)
|
||||
if did_lock:
|
||||
setup_lock.release()
|
||||
|
||||
|
||||
def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_component(hass, config, domain), loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
component = loader.get_component(domain)
|
||||
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
||||
if dep not in hass.config.components]
|
||||
|
||||
if missing_deps:
|
||||
_LOGGER.error(
|
||||
'Not initializing %s because not all dependencies loaded: %s',
|
||||
domain, ", ".join(missing_deps))
|
||||
return None
|
||||
|
||||
if hasattr(component, 'CONFIG_SCHEMA'):
|
||||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
return None
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
platforms = []
|
||||
for p_name, p_config in config_per_platform(config, domain):
|
||||
# Validate component specific platform schema
|
||||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
continue
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
# So if p_name is None we are not going to validate platform
|
||||
# (the automation component is one of them)
|
||||
if p_name is None:
|
||||
platforms.append(p_validated)
|
||||
continue
|
||||
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, domain, p_name)
|
||||
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
# Validate platform specific schema
|
||||
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated, hass)
|
||||
continue
|
||||
|
||||
platforms.append(p_validated)
|
||||
|
||||
# Create a copy of the configuration with all config for current
|
||||
# component removed and add validated config back in.
|
||||
filter_keys = extract_domain_configs(config, domain)
|
||||
config = {key: value for key, value in config.items()
|
||||
if key not in filter_keys}
|
||||
config[domain] = platforms
|
||||
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, component, domain)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) -> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_platform(hass, config, domain, platform_name),
|
||||
loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) \
|
||||
-> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
|
||||
|
||||
platform = loader.get_platform(domain, platform_name)
|
||||
|
||||
# Not found
|
||||
if platform is None:
|
||||
_LOGGER.error('Unable to find platform %s', platform_path)
|
||||
_async_persistent_notification(hass, platform_path)
|
||||
return None
|
||||
|
||||
# Already loaded
|
||||
elif platform_path in hass.config.components:
|
||||
return platform
|
||||
|
||||
# Load dependencies
|
||||
for component in getattr(platform, 'DEPENDENCIES', []):
|
||||
if component in loader.DEPENDENCY_BLACKLIST:
|
||||
raise HomeAssistantError(
|
||||
'{} is not allowed to be a dependency.'.format(component))
|
||||
|
||||
res = yield from async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error(
|
||||
'Unable to prepare setup for platform %s because '
|
||||
'dependency %s could not be initialized', platform_path,
|
||||
component)
|
||||
_async_persistent_notification(hass, platform_path, True)
|
||||
return None
|
||||
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, platform, platform_path)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return platform
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction',
|
||||
'frontend', 'history'))
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
@@ -339,23 +50,14 @@ def from_config_dict(config: Dict[str, Any],
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_dict(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.async_add_job(_async_init_from_config_dict(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
)
|
||||
|
||||
return future.result()
|
||||
return hass
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -372,19 +74,13 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
hass.async_track_tasks()
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
yield from setup_lock.acquire()
|
||||
|
||||
start = time()
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
yield from conf_util.async_process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
yield from hass.loop.run_in_executor(
|
||||
@@ -429,23 +125,24 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
# stage 1
|
||||
for component in components:
|
||||
if component not in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
|
||||
# Setup the components
|
||||
dependency_blacklist = loader.DEPENDENCY_BLACKLIST - set(components)
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
for domain in loader.load_order_components(components):
|
||||
if domain in dependency_blacklist:
|
||||
raise HomeAssistantError(
|
||||
'{} is not allowed to be a dependency'.format(domain))
|
||||
# stage 2
|
||||
for component in components:
|
||||
if component in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
|
||||
yield from _async_setup_component(hass, domain, config)
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
setup_lock.release()
|
||||
|
||||
yield from hass.async_stop_track_tasks()
|
||||
stop = time()
|
||||
_LOGGER.info('Home Assistant initialized in %.2fs', stop-start)
|
||||
|
||||
async_register_signal_handling(hass)
|
||||
return hass
|
||||
@@ -464,22 +161,13 @@ def from_config_file(config_path: str,
|
||||
if hass is None:
|
||||
hass = core.HomeAssistant()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_file(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.loop.create_task(_async_init_from_config_file(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
)
|
||||
|
||||
return future.result()
|
||||
return hass
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -504,7 +192,8 @@ def async_from_config_file(config_path: str,
|
||||
try:
|
||||
config_dict = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError:
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Error loading %s: %s', config_path, err)
|
||||
return None
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
@@ -588,57 +277,6 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
'Unable to setup error log %s (access denied)', err_log_path)
|
||||
|
||||
|
||||
def log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_log_exception, ex, domain, config, hass).result()
|
||||
|
||||
|
||||
@core.callback
|
||||
def _async_persistent_notification(hass: core.HomeAssistant, component: str,
|
||||
link: Optional[bool]=False):
|
||||
"""Print a persistent notification.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
_PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link
|
||||
_lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name)
|
||||
if link else name for name, link in _PERSISTENT_ERRORS.items()]
|
||||
message = ('The following components and platforms could not be set up:\n'
|
||||
'* ' + '\n* '.join(list(_lst)) + '\nPlease check your config')
|
||||
persistent_notification.async_create(
|
||||
hass, message, 'Invalid config', 'invalid_config')
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
if hass is not None:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||
.format(ex.path[-1], domain, domain,
|
||||
'->'.join(str(m) for m in ex.path))
|
||||
else:
|
||||
message += '{}.'.format(humanize_error(config, ex))
|
||||
|
||||
domain_config = config.get(domain, config)
|
||||
message += " (See {}, line {}). ".format(
|
||||
getattr(domain_config, '__config_file__', '?'),
|
||||
getattr(domain_config, '__line__', '?'))
|
||||
|
||||
if domain != 'homeassistant':
|
||||
message += ('Please check the docs at '
|
||||
'https://home-assistant.io/components/{}/'.format(domain))
|
||||
|
||||
_LOGGER.error(message)
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ def async_setup(hass, config):
|
||||
if not alarm.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
alarm.async_update_ha_state(True))
|
||||
if hasattr(alarm, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
119
homeassistant/components/alarm_control_panel/alarmdecoder.py
Normal file
119
homeassistant/components/alarm_control_panel/alarmdecoder.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.components.alarmdecoder import (DATA_AD,
|
||||
SIGNAL_PANEL_MESSAGE)
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Perform the setup for AlarmDecoder alarm panels."""
|
||||
_LOGGER.debug("AlarmDecoderAlarmPanel: setup")
|
||||
|
||||
device = AlarmDecoderAlarmPanel("Alarm Panel", hass)
|
||||
|
||||
async_add_devices([device])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
def __init__(self, name, hass):
|
||||
"""Initialize the alarm panel."""
|
||||
self._display = ""
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
_LOGGER.debug("AlarmDecoderAlarm: Setting up panel")
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
|
||||
@callback
|
||||
def _message_callback(self, message):
|
||||
if message.alarm_sounding or message.fire_alarm:
|
||||
if self._state != STATE_ALARM_TRIGGERED:
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
elif message.armed_away:
|
||||
if self._state != STATE_ALARM_ARMED_AWAY:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
elif message.armed_home:
|
||||
if self._state != STATE_ALARM_ARMED_HOME:
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
else:
|
||||
if self._state != STATE_ALARM_DISARMED:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: sending %s1",
|
||||
str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}1".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: sending %s2",
|
||||
str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}2".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: sending %s3",
|
||||
str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}3".format(code))
|
||||
@@ -1,13 +1,13 @@
|
||||
"""
|
||||
|
||||
Interfaces with Alarm.com alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
@@ -15,10 +15,9 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, CONF_CODE,
|
||||
CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom'
|
||||
'/archive/0.1.1.zip'
|
||||
'#pyalarmdotcom==0.1.1']
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,14 +31,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup an Alarm.com control panel."""
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Alarm.com control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
add_devices([AlarmDotCom(hass, name, code, username, password)], True)
|
||||
alarmdotcom = AlarmDotCom(hass, name, code, username, password)
|
||||
yield from alarmdotcom.async_login()
|
||||
async_add_devices([alarmdotcom])
|
||||
|
||||
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
@@ -47,18 +49,30 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
|
||||
def __init__(self, hass, name, code, username, password):
|
||||
"""Initialize the Alarm.com status."""
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
self._alarm = Alarmdotcom(username, password, timeout=10)
|
||||
from pyalarmdotcom import Alarmdotcom
|
||||
_LOGGER.debug('Setting up Alarm.com...')
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._websession = async_get_clientsession(self._hass)
|
||||
self._state = STATE_UNKNOWN
|
||||
self._alarm = Alarmdotcom(username,
|
||||
password,
|
||||
self._websession,
|
||||
hass.loop)
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_login(self):
|
||||
"""Login to Alarm.com."""
|
||||
yield from self._alarm.async_login()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Fetch the latest state."""
|
||||
self._state = self._alarm.state
|
||||
yield from self._alarm.async_update()
|
||||
return self._alarm.state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -73,45 +87,36 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == 'Disarmed':
|
||||
if self._alarm.state.lower() == 'disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
elif self._state == 'Armed Stay':
|
||||
elif self._alarm.state.lower() == 'armed stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._state == 'Armed Away':
|
||||
elif self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.disarm()
|
||||
if self._validate_code(code):
|
||||
yield from self._alarm.async_alarm_disarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_stay()
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm hom command."""
|
||||
if self._validate_code(code):
|
||||
yield from self._alarm.async_alarm_arm_home()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_away()
|
||||
if self._validate_code(code):
|
||||
yield from self._alarm.async_alarm_arm_away()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
def _validate_code(self, code):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered for %s', state)
|
||||
_LOGGER.warning('Wrong code entered.')
|
||||
return check
|
||||
|
||||
@@ -55,7 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
@callback
|
||||
def alarm_keypress_handler(service):
|
||||
@@ -94,10 +94,13 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self, partition):
|
||||
|
||||
@@ -46,7 +46,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the MQTT platform."""
|
||||
yield from async_add_devices([MqttAlarm(
|
||||
async_add_devices([MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
||||
87
homeassistant/components/alarm_control_panel/totalconnect.py
Normal file
87
homeassistant/components/alarm_control_panel/totalconnect.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Interfaces with TotalConnect alarm control panels."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN,
|
||||
CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Total Connect'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup a TotalConnect control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
total_connect = TotalConnect(name, username, password)
|
||||
add_devices([total_connect], True)
|
||||
|
||||
|
||||
class TotalConnect(alarm.AlarmControlPanel):
|
||||
"""Represent an TotalConnect status."""
|
||||
|
||||
def __init__(self, name, username, password):
|
||||
"""Initialize the TotalConnect status."""
|
||||
from total_connect_client import TotalConnectClient
|
||||
|
||||
_LOGGER.debug('Setting up TotalConnect...')
|
||||
self._name = name
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._state = STATE_UNKNOWN
|
||||
self._client = TotalConnectClient.TotalConnectClient(username,
|
||||
password)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
status = self._client.get_armed_status()
|
||||
|
||||
if status == self._client.DISARMED:
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status == self._client.ARMED_STAY:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif status == self._client.ARMED_AWAY:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
state = STATE_UNKNOWN
|
||||
|
||||
self._state = state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._client.disarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._client.arm_stay()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._client.arm_away()
|
||||
171
homeassistant/components/alarmdecoder.py
Normal file
171
homeassistant/components/alarmdecoder.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Support for AlarmDecoder devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['alarmdecoder==0.12.1.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'alarmdecoder'
|
||||
|
||||
DATA_AD = 'alarmdecoder'
|
||||
|
||||
|
||||
CONF_DEVICE = 'device'
|
||||
CONF_DEVICE_TYPE = 'type'
|
||||
CONF_DEVICE_HOST = 'host'
|
||||
CONF_DEVICE_PORT = 'port'
|
||||
CONF_DEVICE_PATH = 'path'
|
||||
CONF_DEVICE_BAUD = 'baudrate'
|
||||
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_ZONE_NAME = 'name'
|
||||
CONF_ZONE_TYPE = 'type'
|
||||
|
||||
CONF_PANEL_DISPLAY = 'panel_display'
|
||||
|
||||
DEFAULT_DEVICE_TYPE = 'socket'
|
||||
DEFAULT_DEVICE_HOST = 'localhost'
|
||||
DEFAULT_DEVICE_PORT = 10000
|
||||
DEFAULT_DEVICE_PATH = '/dev/ttyUSB0'
|
||||
DEFAULT_DEVICE_BAUD = 115200
|
||||
|
||||
DEFAULT_PANEL_DISPLAY = False
|
||||
|
||||
DEFAULT_ZONE_TYPE = 'opening'
|
||||
|
||||
SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message'
|
||||
SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away'
|
||||
SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home'
|
||||
SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
|
||||
|
||||
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
||||
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
||||
|
||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
||||
vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
|
||||
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port})
|
||||
|
||||
DEVICE_SERIAL_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'serial',
|
||||
vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
|
||||
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string})
|
||||
|
||||
DEVICE_USB_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'usb'})
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONE_NAME): cv.string,
|
||||
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA,
|
||||
DEVICE_SERIAL_SCHEMA,
|
||||
DEVICE_USB_SCHEMA),
|
||||
vol.Optional(CONF_PANEL_DISPLAY,
|
||||
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Common setup for AlarmDecoder devices."""
|
||||
from alarmdecoder import AlarmDecoder
|
||||
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
|
||||
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
device = conf.get(CONF_DEVICE)
|
||||
display = conf.get(CONF_PANEL_DISPLAY)
|
||||
zones = conf.get(CONF_ZONES)
|
||||
|
||||
device_type = device.get(CONF_DEVICE_TYPE)
|
||||
host = DEFAULT_DEVICE_HOST
|
||||
port = DEFAULT_DEVICE_PORT
|
||||
path = DEFAULT_DEVICE_PATH
|
||||
baud = DEFAULT_DEVICE_BAUD
|
||||
|
||||
sync_connect = asyncio.Future(loop=hass.loop)
|
||||
|
||||
def handle_open(device):
|
||||
"""Callback for a successful connection."""
|
||||
_LOGGER.info("Established a connection with the alarmdecoder.")
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
||||
sync_connect.set_result(True)
|
||||
|
||||
@callback
|
||||
def stop_alarmdecoder(event):
|
||||
"""Callback to handle shutdown alarmdecoder."""
|
||||
_LOGGER.debug("Shutting down alarmdecoder.")
|
||||
controller.close()
|
||||
|
||||
@callback
|
||||
def handle_message(sender, message):
|
||||
"""Callback to handle message from alarmdecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message)
|
||||
|
||||
def zone_fault_callback(sender, zone):
|
||||
"""Callback to handle zone fault from alarmdecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone)
|
||||
|
||||
def zone_restore_callback(sender, zone):
|
||||
"""Callback to handle zone restore from alarmdecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone)
|
||||
|
||||
controller = False
|
||||
if device_type == 'socket':
|
||||
host = device.get(CONF_DEVICE_HOST)
|
||||
port = device.get(CONF_DEVICE_PORT)
|
||||
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
|
||||
elif device_type == 'serial':
|
||||
path = device.get(CONF_DEVICE_PATH)
|
||||
baud = device.get(CONF_DEVICE_BAUD)
|
||||
controller = AlarmDecoder(SerialDevice(interface=path))
|
||||
elif device_type == 'usb':
|
||||
AlarmDecoder(USBDevice.find())
|
||||
return False
|
||||
|
||||
controller.on_open += handle_open
|
||||
controller.on_message += handle_message
|
||||
controller.on_zone_fault += zone_fault_callback
|
||||
controller.on_zone_restore += zone_restore_callback
|
||||
|
||||
hass.data[DATA_AD] = controller
|
||||
|
||||
controller.open(baud)
|
||||
|
||||
result = yield from sync_connect
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
hass.async_add_job(async_load_platform(hass, 'alarm_control_panel', DOMAIN,
|
||||
conf, config))
|
||||
|
||||
if zones:
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config))
|
||||
|
||||
if display:
|
||||
hass.async_add_job(async_load_platform(hass, 'sensor', DOMAIN,
|
||||
conf, config))
|
||||
|
||||
return True
|
||||
@@ -18,7 +18,6 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers import service, event
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -62,8 +61,7 @@ def is_on(hass, entity_id):
|
||||
|
||||
def turn_on(hass, entity_id):
|
||||
"""Reset the alert."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_turn_on, hass, entity_id).result()
|
||||
hass.add_job(async_turn_on, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -76,8 +74,7 @@ def async_turn_on(hass, entity_id):
|
||||
|
||||
def turn_off(hass, entity_id):
|
||||
"""Acknowledge alert."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_turn_off, hass, entity_id).result()
|
||||
hass.add_job(async_turn_off, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -90,7 +87,7 @@ def async_turn_off(hass, entity_id):
|
||||
|
||||
def toggle(hass, entity_id):
|
||||
"""Toggle acknowledgement of alert."""
|
||||
run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id)
|
||||
hass.add_job(async_toggle, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
293
homeassistant/components/android_ip_webcam.py
Normal file
293
homeassistant/components/android_ip_webcam.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Support for IP Webcam, an Android app that acts as a full-featured webcam.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/android_ip_webcam/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL,
|
||||
CONF_PLATFORM)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send, async_dispatcher_connect)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
||||
|
||||
REQUIREMENTS = ['pydroid-ipcam==0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AUD_CONNS = 'Audio Connections'
|
||||
ATTR_HOST = 'host'
|
||||
ATTR_VID_CONNS = 'Video Connections'
|
||||
|
||||
CONF_MOTION_SENSOR = 'motion_sensor'
|
||||
|
||||
DATA_IP_WEBCAM = 'android_ip_webcam'
|
||||
DEFAULT_NAME = 'IP Webcam'
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_TIMEOUT = 10
|
||||
DOMAIN = 'android_ip_webcam'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
||||
|
||||
KEY_MAP = {
|
||||
'audio_connections': 'Audio Connections',
|
||||
'adet_limit': 'Audio Trigger Limit',
|
||||
'antibanding': 'Anti-banding',
|
||||
'audio_only': 'Audio Only',
|
||||
'battery_level': 'Battery Level',
|
||||
'battery_temp': 'Battery Temperature',
|
||||
'battery_voltage': 'Battery Voltage',
|
||||
'coloreffect': 'Color Effect',
|
||||
'exposure': 'Exposure Level',
|
||||
'exposure_lock': 'Exposure Lock',
|
||||
'ffc': 'Front-facing Camera',
|
||||
'flashmode': 'Flash Mode',
|
||||
'focus': 'Focus',
|
||||
'focus_homing': 'Focus Homing',
|
||||
'focus_region': 'Focus Region',
|
||||
'focusmode': 'Focus Mode',
|
||||
'gps_active': 'GPS Active',
|
||||
'idle': 'Idle',
|
||||
'ip_address': 'IPv4 Address',
|
||||
'ipv6_address': 'IPv6 Address',
|
||||
'ivideon_streaming': 'Ivideon Streaming',
|
||||
'light': 'Light Level',
|
||||
'mirror_flip': 'Mirror Flip',
|
||||
'motion': 'Motion',
|
||||
'motion_active': 'Motion Active',
|
||||
'motion_detect': 'Motion Detection',
|
||||
'motion_event': 'Motion Event',
|
||||
'motion_limit': 'Motion Limit',
|
||||
'night_vision': 'Night Vision',
|
||||
'night_vision_average': 'Night Vision Average',
|
||||
'night_vision_gain': 'Night Vision Gain',
|
||||
'orientation': 'Orientation',
|
||||
'overlay': 'Overlay',
|
||||
'photo_size': 'Photo Size',
|
||||
'pressure': 'Pressure',
|
||||
'proximity': 'Proximity',
|
||||
'quality': 'Quality',
|
||||
'scenemode': 'Scene Mode',
|
||||
'sound': 'Sound',
|
||||
'sound_event': 'Sound Event',
|
||||
'sound_timeout': 'Sound Timeout',
|
||||
'torch': 'Torch',
|
||||
'video_connections': 'Video Connections',
|
||||
'video_chunk_len': 'Video Chunk Length',
|
||||
'video_recording': 'Video Recording',
|
||||
'video_size': 'Video Size',
|
||||
'whitebalance': 'White Balance',
|
||||
'whitebalance_lock': 'White Balance Lock',
|
||||
'zoom': 'Zoom'
|
||||
}
|
||||
|
||||
ICON_MAP = {
|
||||
'audio_connections': 'mdi:speaker',
|
||||
'battery_level': 'mdi:battery',
|
||||
'battery_temp': 'mdi:thermometer',
|
||||
'battery_voltage': 'mdi:battery-charging-100',
|
||||
'exposure_lock': 'mdi:camera',
|
||||
'ffc': 'mdi:camera-front-variant',
|
||||
'focus': 'mdi:image-filter-center-focus',
|
||||
'gps_active': 'mdi:crosshairs-gps',
|
||||
'light': 'mdi:flashlight',
|
||||
'motion': 'mdi:run',
|
||||
'night_vision': 'mdi:weather-night',
|
||||
'overlay': 'mdi:monitor',
|
||||
'pressure': 'mdi:gauge',
|
||||
'proximity': 'mdi:map-marker-radius',
|
||||
'quality': 'mdi:quality-high',
|
||||
'sound': 'mdi:speaker',
|
||||
'sound_event': 'mdi:speaker',
|
||||
'sound_timeout': 'mdi:speaker',
|
||||
'torch': 'mdi:white-balance-sunny',
|
||||
'video_chunk_len': 'mdi:video',
|
||||
'video_connections': 'mdi:eye',
|
||||
'video_recording': 'mdi:record-rec',
|
||||
'whitebalance_lock': 'mdi:white-balance-auto'
|
||||
}
|
||||
|
||||
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision',
|
||||
'overlay', 'torch', 'whitebalance_lock', 'video_recording']
|
||||
|
||||
SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
||||
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
|
||||
'sound', 'video_connections']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_SWITCHES, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(CONF_MOTION_SENSOR, default=None): cv.boolean,
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the IP Webcam component."""
|
||||
from pydroid_ipcam import PyDroidIPCam
|
||||
|
||||
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_ipcamera(cam_config):
|
||||
"""Set up an IP camera."""
|
||||
host = cam_config[CONF_HOST]
|
||||
username = cam_config.get(CONF_USERNAME)
|
||||
password = cam_config.get(CONF_PASSWORD)
|
||||
name = cam_config[CONF_NAME]
|
||||
interval = cam_config[CONF_SCAN_INTERVAL]
|
||||
switches = cam_config[CONF_SWITCHES]
|
||||
sensors = cam_config[CONF_SENSORS]
|
||||
motion = cam_config[CONF_MOTION_SENSOR]
|
||||
|
||||
# Init ip webcam
|
||||
cam = PyDroidIPCam(
|
||||
hass.loop, websession, host, cam_config[CONF_PORT],
|
||||
username=username, password=password,
|
||||
timeout=cam_config[CONF_TIMEOUT]
|
||||
)
|
||||
|
||||
if switches is None:
|
||||
switches = [setting for setting in cam.enabled_settings
|
||||
if setting in SWITCHES]
|
||||
|
||||
if sensors is None:
|
||||
sensors = [sensor for sensor in cam.enabled_sensors
|
||||
if sensor in SENSORS]
|
||||
sensors.extend(['audio_connections', 'video_connections'])
|
||||
|
||||
if motion is None:
|
||||
motion = 'motion_active' in cam.enabled_sensors
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_data(now):
|
||||
"""Update data from IP camera in SCAN_INTERVAL."""
|
||||
yield from cam.update()
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
||||
|
||||
async_track_point_in_utc_time(
|
||||
hass, async_update_data, utcnow() + interval)
|
||||
|
||||
yield from async_update_data(None)
|
||||
|
||||
# Load platforms
|
||||
webcams[host] = cam
|
||||
|
||||
mjpeg_camera = {
|
||||
CONF_PLATFORM: 'mjpeg',
|
||||
CONF_MJPEG_URL: cam.mjpeg_url,
|
||||
CONF_STILL_IMAGE_URL: cam.image_url,
|
||||
CONF_NAME: name,
|
||||
}
|
||||
if username and password:
|
||||
mjpeg_camera.update({
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password
|
||||
})
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'camera', 'mjpeg', mjpeg_camera, config))
|
||||
|
||||
if sensors:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config))
|
||||
|
||||
if switches:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
CONF_SWITCHES: switches,
|
||||
}, config))
|
||||
|
||||
if motion:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
CONF_HOST: host,
|
||||
CONF_NAME: name,
|
||||
}, config))
|
||||
|
||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AndroidIPCamEntity(Entity):
|
||||
"""The Android device running IP Webcam."""
|
||||
|
||||
def __init__(self, host, ipcam):
|
||||
"""Initialize the data oject."""
|
||||
self._host = host
|
||||
self._ipcam = ipcam
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register update dispatcher."""
|
||||
@callback
|
||||
def async_ipcam_update(host):
|
||||
"""Update callback."""
|
||||
if self._host != host:
|
||||
return
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._ipcam.available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {ATTR_HOST: self._host}
|
||||
if self._ipcam.status_data is None:
|
||||
return state_attr
|
||||
|
||||
state_attr[ATTR_VID_CONNS] = \
|
||||
self._ipcam.status_data.get('video_connections')
|
||||
state_attr[ATTR_AUD_CONNS] = \
|
||||
self._ipcam.status_data.get('audio_connections')
|
||||
|
||||
return state_attr
|
||||
@@ -17,9 +17,9 @@ from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
|
||||
HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
|
||||
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_SERVICES,
|
||||
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
|
||||
@@ -48,11 +48,12 @@ def setup(hass, config):
|
||||
hass.http.register_view(APIEventView)
|
||||
hass.http.register_view(APIServicesView)
|
||||
hass.http.register_view(APIDomainServicesView)
|
||||
hass.http.register_view(APIEventForwardingView)
|
||||
hass.http.register_view(APIComponentsView)
|
||||
hass.http.register_view(APIErrorLogView)
|
||||
hass.http.register_view(APITemplateView)
|
||||
|
||||
hass.http.register_static_path(
|
||||
URL_API_ERROR_LOG, hass.config.path(ERROR_LOG_FILENAME), False)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -317,77 +318,6 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
return self.json(changed_states)
|
||||
|
||||
|
||||
class APIEventForwardingView(HomeAssistantView):
|
||||
"""View to handle EventForwarding requests."""
|
||||
|
||||
url = URL_API_EVENT_FORWARD
|
||||
name = "api:event-forward"
|
||||
event_forwarder = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
except KeyError:
|
||||
return self.json_message("No host or api_password received.",
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
return self.json_message("Invalid value received for port.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
valid = yield from hass.loop.run_in_executor(
|
||||
None, api.validate_api)
|
||||
if not valid:
|
||||
return self.json_message("Unable to validate API.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self.event_forwarder is None:
|
||||
self.event_forwarder = rem.EventForwarder(hass)
|
||||
|
||||
self.event_forwarder.async_connect(api)
|
||||
|
||||
return self.json_message("Event forwarding setup.")
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete(self, request):
|
||||
"""Remove event forwarder."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
return self.json_message("No host received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
return self.json_message("Invalid value received for port.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
self.event_forwarder.async_disconnect(api)
|
||||
|
||||
return self.json_message("Event forwarding cancelled.")
|
||||
|
||||
|
||||
class APIComponentsView(HomeAssistantView):
|
||||
"""View to handle Components requests."""
|
||||
|
||||
@@ -400,20 +330,6 @@ class APIComponentsView(HomeAssistantView):
|
||||
return self.json(request.app['hass'].config.components)
|
||||
|
||||
|
||||
class APIErrorLogView(HomeAssistantView):
|
||||
"""View to handle ErrorLog requests."""
|
||||
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error-log"
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Serve error log."""
|
||||
resp = yield from self.file(
|
||||
request, request.app['hass'].config.path(ERROR_LOG_FILENAME))
|
||||
return resp
|
||||
|
||||
|
||||
class APITemplateView(HomeAssistantView):
|
||||
"""View to handle requests."""
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
from homeassistant.const import CONF_PORT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['PyMata==2.13']
|
||||
REQUIREMENTS = ['PyMata==2.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,18 +29,25 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Arduino component."""
|
||||
"""Set up the Arduino component."""
|
||||
import serial
|
||||
|
||||
port = config[DOMAIN][CONF_PORT]
|
||||
|
||||
global BOARD
|
||||
try:
|
||||
BOARD = ArduinoBoard(config[DOMAIN][CONF_PORT])
|
||||
BOARD = ArduinoBoard(port)
|
||||
except (serial.serialutil.SerialException, FileNotFoundError):
|
||||
_LOGGER.exception("Your port is not accessible.")
|
||||
_LOGGER.error("Your port %s is not accessible", port)
|
||||
return False
|
||||
|
||||
if BOARD.get_firmata()[1] <= 2:
|
||||
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer.")
|
||||
return False
|
||||
try:
|
||||
if BOARD.get_firmata()[1] <= 2:
|
||||
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer")
|
||||
return False
|
||||
except IndexError:
|
||||
_LOGGER.warning("The version of the StandardFirmata sketch was not"
|
||||
"detected. This may lead to side effects")
|
||||
|
||||
def stop_arduino(event):
|
||||
"""Stop the Arduino service."""
|
||||
@@ -67,25 +74,20 @@ class ArduinoBoard(object):
|
||||
def set_mode(self, pin, direction, mode):
|
||||
"""Set the mode and the direction of a given pin."""
|
||||
if mode == 'analog' and direction == 'in':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.INPUT,
|
||||
self._board.ANALOG)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.INPUT, self._board.ANALOG)
|
||||
elif mode == 'analog' and direction == 'out':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.OUTPUT,
|
||||
self._board.ANALOG)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.OUTPUT, self._board.ANALOG)
|
||||
elif mode == 'digital' and direction == 'in':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.INPUT,
|
||||
self._board.DIGITAL)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.INPUT, self._board.DIGITAL)
|
||||
elif mode == 'digital' and direction == 'out':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.OUTPUT,
|
||||
self._board.DIGITAL)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.OUTPUT, self._board.DIGITAL)
|
||||
elif mode == 'pwm':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.OUTPUT,
|
||||
self._board.PWM)
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.OUTPUT, self._board.PWM)
|
||||
|
||||
def get_analog_inputs(self):
|
||||
"""Get the values from the pins."""
|
||||
|
||||
@@ -11,16 +11,18 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import async_prepare_setup_platform
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import CoreState
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE)
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.loader import get_platform
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -28,8 +30,6 @@ import homeassistant.helpers.config_validation as cv
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DEPENDENCIES = ['group']
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
@@ -52,7 +52,6 @@ DEFAULT_INITIAL_STATE = True
|
||||
ATTR_LAST_TRIGGERED = 'last_triggered'
|
||||
ATTR_VARIABLES = 'variables'
|
||||
SERVICE_TRIGGER = 'trigger'
|
||||
SERVICE_RELOAD = 'reload'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -83,8 +82,7 @@ _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
CONF_ALIAS: cv.string,
|
||||
vol.Optional(CONF_INITIAL_STATE,
|
||||
default=DEFAULT_INITIAL_STATE): cv.boolean,
|
||||
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
|
||||
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
||||
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
||||
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
||||
@@ -103,15 +101,13 @@ TRIGGER_SERVICE_SCHEMA = vol.Schema({
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
def is_on(hass, entity_id):
|
||||
"""
|
||||
Return true if specified automation entity_id is on.
|
||||
|
||||
Check all automation if no entity_id specified.
|
||||
Async friendly.
|
||||
"""
|
||||
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
|
||||
return any(hass.states.is_state(entity_id, STATE_ON)
|
||||
for entity_id in entity_ids)
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
def turn_on(hass, entity_id=None):
|
||||
@@ -226,16 +222,16 @@ class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||
hidden):
|
||||
hidden, initial_state):
|
||||
"""Initialize an automation entity."""
|
||||
self._name = name
|
||||
self._async_attach_triggers = async_attach_triggers
|
||||
self._async_detach_triggers = None
|
||||
self._cond_func = cond_func
|
||||
self._async_action = async_action
|
||||
self._enabled = False
|
||||
self._last_triggered = None
|
||||
self._hidden = hidden
|
||||
self._initial_state = initial_state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -262,26 +258,62 @@ class AutomationEntity(ToggleEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self._enabled
|
||||
return self._async_detach_triggers is not None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Startup with initial state or previous state."""
|
||||
if self._initial_state is not None:
|
||||
enable_automation = self._initial_state
|
||||
_LOGGER.debug("Automation %s initial state %s from config "
|
||||
"initial_state", self.entity_id, enable_automation)
|
||||
else:
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
enable_automation = state.state == STATE_ON
|
||||
self._last_triggered = state.attributes.get('last_triggered')
|
||||
_LOGGER.debug("Automation %s initial state %s from recorder "
|
||||
"last state %s", self.entity_id,
|
||||
enable_automation, state)
|
||||
else:
|
||||
enable_automation = DEFAULT_INITIAL_STATE
|
||||
_LOGGER.debug("Automation %s initial state %s from default "
|
||||
"initial state", self.entity_id,
|
||||
enable_automation)
|
||||
|
||||
if not enable_automation:
|
||||
return
|
||||
|
||||
# HomeAssistant is starting up
|
||||
elif self.hass.state == CoreState.not_running:
|
||||
@asyncio.coroutine
|
||||
def async_enable_automation(event):
|
||||
"""Start automation on startup."""
|
||||
yield from self.async_enable()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, async_enable_automation)
|
||||
|
||||
# HomeAssistant is running
|
||||
else:
|
||||
yield from self.async_enable()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on and update the state."""
|
||||
if self._enabled:
|
||||
if self.is_on:
|
||||
return
|
||||
|
||||
yield from self.async_enable()
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the entity off."""
|
||||
if not self._enabled:
|
||||
if not self.is_on:
|
||||
return
|
||||
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
self._enabled = False
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -307,12 +339,12 @@ class AutomationEntity(ToggleEntity):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if self._enabled:
|
||||
if self.is_on:
|
||||
return
|
||||
|
||||
self._async_detach_triggers = yield from self._async_attach_triggers(
|
||||
self.async_trigger)
|
||||
self._enabled = True
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -322,7 +354,6 @@ def _async_process_config(hass, config, component):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
entities = []
|
||||
tasks = []
|
||||
|
||||
for config_key in extract_domain_configs(config, DOMAIN):
|
||||
conf = config[config_key]
|
||||
@@ -332,6 +363,7 @@ def _async_process_config(hass, config, component):
|
||||
list_no)
|
||||
|
||||
hidden = config_block[CONF_HIDE_ENTITY]
|
||||
initial_state = config_block.get(CONF_INITIAL_STATE)
|
||||
|
||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
||||
name)
|
||||
@@ -348,15 +380,14 @@ def _async_process_config(hass, config, component):
|
||||
|
||||
async_attach_triggers = partial(
|
||||
_async_process_trigger, hass, config,
|
||||
config_block.get(CONF_TRIGGER, []), name)
|
||||
entity = AutomationEntity(name, async_attach_triggers, cond_func,
|
||||
action, hidden)
|
||||
if config_block[CONF_INITIAL_STATE]:
|
||||
tasks.append(entity.async_enable())
|
||||
config_block.get(CONF_TRIGGER, []), name
|
||||
)
|
||||
entity = AutomationEntity(
|
||||
name, async_attach_triggers, cond_func, action, hidden,
|
||||
initial_state)
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
Offer event listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#event-trigger
|
||||
at https://home-assistant.io/docs/automation/trigger/#event-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.core import callback, CoreState
|
||||
from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
CONF_EVENT_TYPE = "event_type"
|
||||
@@ -31,6 +31,19 @@ def async_trigger(hass, config, action):
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data = config.get(CONF_EVENT_DATA)
|
||||
|
||||
if (event_type == EVENT_HOMEASSISTANT_START and
|
||||
hass.state == CoreState.starting):
|
||||
_LOGGER.warning('Deprecation: Automations should not listen to event '
|
||||
"'homeassistant_start'. Use platform 'homeassistant' "
|
||||
'instead. Feature will be removed in 0.45')
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': None,
|
||||
},
|
||||
})
|
||||
return lambda: None
|
||||
|
||||
@callback
|
||||
def handle_event(event):
|
||||
"""Listen for events and calls the action when data matches."""
|
||||
|
||||
55
homeassistant/components/automation/homeassistant.py
Normal file
55
homeassistant/components/automation/homeassistant.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Offer Home Assistant core automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#homeassistant-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback, CoreState
|
||||
from homeassistant.const import (
|
||||
CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
EVENT_START = 'start'
|
||||
EVENT_SHUTDOWN = 'shutdown'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'homeassistant',
|
||||
vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN),
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
if event == EVENT_SHUTDOWN:
|
||||
@callback
|
||||
def hass_shutdown(event):
|
||||
"""Called when Home Assistant is shutting down."""
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'homeassistant',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
|
||||
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
hass_shutdown)
|
||||
|
||||
# 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:
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'homeassistant',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
|
||||
return lambda: None
|
||||
@@ -70,7 +70,7 @@ def async_trigger(hass, config, action):
|
||||
nonlocal held_less_than, held_more_than
|
||||
pressed_time = dt_util.utcnow()
|
||||
if held_more_than is None and held_less_than is None:
|
||||
call_action()
|
||||
hass.add_job(call_action)
|
||||
if held_more_than is not None and held_less_than is None:
|
||||
cancel_pressed_more_than = track_point_in_utc_time(
|
||||
hass,
|
||||
@@ -88,7 +88,7 @@ def async_trigger(hass, config, action):
|
||||
held_time = dt_util.utcnow() - pressed_time
|
||||
if held_less_than is not None and held_time < held_less_than:
|
||||
if held_more_than is None or held_time > held_more_than:
|
||||
call_action()
|
||||
hass.add_job(call_action)
|
||||
|
||||
hass.data['litejet_system'].on_switch_pressed(number, pressed)
|
||||
hass.data['litejet_system'].on_switch_released(number, released)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Offer MQTT listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||
at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Offer numeric state listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
||||
at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Offer state listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#state-trigger
|
||||
at https://home-assistant.io/docs/automation/trigger/#state-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Offer sun based automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#sun-trigger
|
||||
at https://home-assistant.io/docs/automation/trigger/#sun-trigger
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Offer template automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#template-trigger
|
||||
at https://home-assistant.io/docs/automation/trigger/#template-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Offer time listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#time-trigger
|
||||
at https://home-assistant.io/docs/automation/trigger/#time-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Offer zone automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#zone-trigger
|
||||
at https://home-assistant.io/docs/automation/trigger/#zone-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
123
homeassistant/components/binary_sensor/alarmdecoder.py
Normal file
123
homeassistant/components/binary_sensor/alarmdecoder.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Support for AlarmDecoder zone states- represented as binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_OPEN, STATE_CLOSED)
|
||||
from homeassistant.components.alarmdecoder import (ZONE_SCHEMA,
|
||||
CONF_ZONES,
|
||||
CONF_ZONE_NAME,
|
||||
CONF_ZONE_TYPE,
|
||||
SIGNAL_ZONE_FAULT,
|
||||
SIGNAL_ZONE_RESTORE)
|
||||
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup AlarmDecoder binary sensor devices."""
|
||||
configured_zones = discovery_info[CONF_ZONES]
|
||||
|
||||
devices = []
|
||||
|
||||
for zone_num in configured_zones:
|
||||
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
device = AlarmDecoderBinarySensor(hass,
|
||||
zone_num,
|
||||
zone_name,
|
||||
zone_type)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an AlarmDecoder binary sensor."""
|
||||
|
||||
def __init__(self, hass, zone_number, zone_name, zone_type):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._zone_type = zone_type
|
||||
self._state = 0
|
||||
self._name = zone_name
|
||||
self._type = zone_type
|
||||
|
||||
_LOGGER.debug('AlarmDecoderBinarySensor: Setup up zone: ' + zone_name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
if self._type == 'opening':
|
||||
return STATE_OPEN if self.is_on else STATE_CLOSED
|
||||
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon for device by its type."""
|
||||
if "window" in self._name.lower():
|
||||
return "mdi:window-open" if self.is_on else "mdi:window-closed"
|
||||
|
||||
if self._type == 'smoke':
|
||||
return "mdi:fire"
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._state == 1
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@callback
|
||||
def _fault_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 1
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@callback
|
||||
def _restore_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 0
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
62
homeassistant/components/binary_sensor/android_ip_webcam.py
Normal file
62
homeassistant/components/binary_sensor/android_ip_webcam.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Support for IP Webcam binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.android_ip_webcam/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.android_ip_webcam import (
|
||||
KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME)
|
||||
|
||||
DEPENDENCIES = ['android_ip_webcam']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup IP Webcam binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
host = discovery_info[CONF_HOST]
|
||||
name = discovery_info[CONF_NAME]
|
||||
ipcam = hass.data[DATA_IP_WEBCAM][host]
|
||||
|
||||
async_add_devices(
|
||||
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)
|
||||
|
||||
|
||||
class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
||||
"""Represents an IP Webcam binary sensor."""
|
||||
|
||||
def __init__(self, name, host, ipcam, sensor):
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(host, ipcam)
|
||||
|
||||
self._sensor = sensor
|
||||
self._mapped_name = KEY_MAP.get(self._sensor, self._sensor)
|
||||
self._name = '{} {}'.format(name, self._mapped_name)
|
||||
self._state = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor, if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
state, _ = self._ipcam.export_sensor(self._sensor)
|
||||
self._state = state == 1.0
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'motion'
|
||||
74
homeassistant/components/binary_sensor/blink.py
Normal file
74
homeassistant/components/binary_sensor/blink.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Support for Blink system camera control.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.blink/
|
||||
"""
|
||||
from homeassistant.components.blink import DOMAIN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the blink binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[DOMAIN].blink
|
||||
devs = list()
|
||||
for name in data.cameras:
|
||||
devs.append(BlinkCameraMotionSensor(name, data))
|
||||
devs.append(BlinkSystemSensor(data))
|
||||
add_devices(devs, True)
|
||||
|
||||
|
||||
class BlinkCameraMotionSensor(BinarySensorDevice):
|
||||
"""A representation of a Blink binary sensor."""
|
||||
|
||||
def __init__(self, name, data):
|
||||
"""Initialize the sensor."""
|
||||
self._name = 'blink_' + name + '_motion_enabled'
|
||||
self._camera_name = name
|
||||
self.data = data
|
||||
self._state = self.data.cameras[self._camera_name].armed
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blink sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update sensor state."""
|
||||
self.data.refresh()
|
||||
self._state = self.data.cameras[self._camera_name].armed
|
||||
|
||||
|
||||
class BlinkSystemSensor(BinarySensorDevice):
|
||||
"""A representation of a Blink system sensor."""
|
||||
|
||||
def __init__(self, data):
|
||||
"""Initialize the sensor."""
|
||||
self._name = 'blink armed status'
|
||||
self.data = data
|
||||
self._state = self.data.arm
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blink sensor."""
|
||||
return self._name.replace(" ", "_")
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update sensor state."""
|
||||
self.data.refresh()
|
||||
self._state = self.data.arm
|
||||
@@ -36,6 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
dev = []
|
||||
for droplet in droplets:
|
||||
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
|
||||
if droplet_id is None:
|
||||
_LOGGER.error("Droplet %s is not available", droplet)
|
||||
return False
|
||||
dev.append(DigitalOceanBinarySensor(
|
||||
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
This method is called when there is an incoming packet associated
|
||||
with this platform.
|
||||
"""
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
if value2 == 0x70:
|
||||
self.which = 0
|
||||
self.onoff = 0
|
||||
|
||||
@@ -37,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
@@ -52,8 +52,11 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
super().__init__(zone_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
||||
@@ -57,16 +57,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegMotion(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Constructor for binary sensor noise detection."""
|
||||
super().__init__(config.get(CONF_INITIAL_STATE))
|
||||
|
||||
@@ -98,15 +95,19 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||
"""Initialize ffmpeg motion binary sensor."""
|
||||
from haffmpeg import SensorMotion
|
||||
|
||||
super().__init__(hass, config)
|
||||
super().__init__(config)
|
||||
self.ffmpeg = SensorMotion(
|
||||
manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_start_ffmpeg(self, entity_ids):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if entity_ids is not None and self.entity_id not in entity_ids:
|
||||
return
|
||||
|
||||
# init config
|
||||
self.ffmpeg.set_options(
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
@@ -116,7 +117,7 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
yield from self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@@ -54,10 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegNoise(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegNoise(FFmpegBinarySensor):
|
||||
@@ -67,15 +64,19 @@ class FFmpegNoise(FFmpegBinarySensor):
|
||||
"""Initialize ffmpeg noise binary sensor."""
|
||||
from haffmpeg import SensorNoise
|
||||
|
||||
super().__init__(hass, config)
|
||||
super().__init__(config)
|
||||
self.ffmpeg = SensorNoise(
|
||||
manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_start_ffmpeg(self, entity_ids):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if entity_ids is not None and self.entity_id not in entity_ids:
|
||||
return
|
||||
|
||||
# init config
|
||||
self.ffmpeg.set_options(
|
||||
time_duration=self._config.get(CONF_DURATION),
|
||||
@@ -84,7 +85,7 @@ class FFmpegNoise(FFmpegBinarySensor):
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
yield from self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
output_dest=self._config.get(CONF_OUTPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
"""Contains functionality to use flic buttons as a binary sensor."""
|
||||
"""
|
||||
Support to use flic buttons as a binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.flic/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
|
||||
@@ -11,39 +16,40 @@ 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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TIMEOUT = 3
|
||||
|
||||
CLICK_TYPE_SINGLE = "single"
|
||||
CLICK_TYPE_DOUBLE = "double"
|
||||
CLICK_TYPE_HOLD = "hold"
|
||||
CLICK_TYPE_SINGLE = 'single'
|
||||
CLICK_TYPE_DOUBLE = 'double'
|
||||
CLICK_TYPE_HOLD = 'hold'
|
||||
CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD]
|
||||
|
||||
CONF_IGNORED_CLICK_TYPES = "ignored_click_types"
|
||||
CONF_IGNORED_CLICK_TYPES = 'ignored_click_types'
|
||||
|
||||
EVENT_NAME = "flic_click"
|
||||
EVENT_DATA_NAME = "button_name"
|
||||
EVENT_DATA_ADDRESS = "button_address"
|
||||
EVENT_DATA_TYPE = "click_type"
|
||||
EVENT_DATA_QUEUED_TIME = "queued_time"
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 5551
|
||||
|
||||
EVENT_NAME = 'flic_click'
|
||||
EVENT_DATA_NAME = 'button_name'
|
||||
EVENT_DATA_ADDRESS = 'button_address'
|
||||
EVENT_DATA_TYPE = 'click_type'
|
||||
EVENT_DATA_QUEUED_TIME = 'queued_time'
|
||||
|
||||
# Validation of the user's configuration
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||
vol.Optional(CONF_PORT, default=5551): cv.port,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All(cv.ensure_list,
|
||||
[vol.In(CLICK_TYPES)])
|
||||
vol.Optional(CONF_IGNORED_CLICK_TYPES):
|
||||
vol.All(cv.ensure_list, [vol.In(CLICK_TYPES)])
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup the flic platform."""
|
||||
"""Set up the flic platform."""
|
||||
import pyflic
|
||||
|
||||
# Initialize flic client responsible for
|
||||
@@ -55,11 +61,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
try:
|
||||
client = pyflic.FlicClient(host, port)
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.error("Failed to connect to flic server.")
|
||||
_LOGGER.error("Failed to connect to flic server")
|
||||
return
|
||||
|
||||
def new_button_callback(address):
|
||||
"""Setup newly verified button as device in home assistant."""
|
||||
"""Set up newly verified button as device in Home Assistant."""
|
||||
setup_button(hass, config, add_entities, client, address)
|
||||
|
||||
client.on_new_verified_button = new_button_callback
|
||||
@@ -74,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
def get_info_callback(items):
|
||||
"""Add entities for already verified buttons."""
|
||||
addresses = items["bd_addr_of_verified_buttons"] or []
|
||||
addresses = items['bd_addr_of_verified_buttons'] or []
|
||||
for address in addresses:
|
||||
setup_button(hass, config, add_entities, client, address)
|
||||
|
||||
@@ -83,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
|
||||
def start_scanning(config, add_entities, client):
|
||||
"""Start a new flic client for scanning & connceting to new buttons."""
|
||||
"""Start a new flic client for scanning and connecting to new buttons."""
|
||||
import pyflic
|
||||
|
||||
scan_wizard = pyflic.ScanWizard()
|
||||
@@ -91,10 +97,10 @@ def start_scanning(config, add_entities, client):
|
||||
def scan_completed_callback(scan_wizard, result, address, name):
|
||||
"""Restart scan wizard to constantly check for new buttons."""
|
||||
if result == pyflic.ScanWizardResult.WizardSuccess:
|
||||
_LOGGER.info("Found new button (%s)", address)
|
||||
_LOGGER.info("Found new button %s", address)
|
||||
elif result != pyflic.ScanWizardResult.WizardFailedTimeout:
|
||||
_LOGGER.warning("Failed to connect to button (%s). Reason: %s",
|
||||
address, result)
|
||||
_LOGGER.warning(
|
||||
"Failed to connect to button %s. Reason: %s", address, result)
|
||||
|
||||
# Restart scan wizard
|
||||
start_scanning(config, add_entities, client)
|
||||
@@ -108,7 +114,7 @@ def setup_button(hass, config, add_entities, client, address):
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES)
|
||||
button = FlicButton(hass, client, address, timeout, ignored_click_types)
|
||||
_LOGGER.info("Connected to button (%s)", address)
|
||||
_LOGGER.info("Connected to button %s", address)
|
||||
|
||||
add_entities([button])
|
||||
|
||||
@@ -161,7 +167,7 @@ class FlicButton(BinarySensorDevice):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return "flic_%s" % self.address.replace(":", "")
|
||||
return 'flic_{}'.format(self.address.replace(':', ''))
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
@@ -181,21 +187,21 @@ class FlicButton(BinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
return {"address": self.address}
|
||||
return {'address': self.address}
|
||||
|
||||
def _queued_event_check(self, click_type, time_diff):
|
||||
"""Generate a log message and returns true if timeout exceeded."""
|
||||
time_string = "{:d} {}".format(
|
||||
time_diff, "second" if time_diff == 1 else "seconds")
|
||||
time_diff, 'second' if time_diff == 1 else 'seconds')
|
||||
|
||||
if time_diff > self._timeout:
|
||||
_LOGGER.warning(
|
||||
"Queued %s dropped for %s. Time in queue was %s.",
|
||||
"Queued %s dropped for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return True
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s.",
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
|
||||
@@ -227,8 +233,8 @@ class FlicButton(BinarySensorDevice):
|
||||
EVENT_DATA_TYPE: hass_click_type
|
||||
})
|
||||
|
||||
def _connection_status_changed(self, channel,
|
||||
connection_status, disconnect_reason):
|
||||
def _connection_status_changed(
|
||||
self, channel, connection_status, disconnect_reason):
|
||||
"""Remove device, if button disconnects."""
|
||||
import pyflic
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ from homeassistant.components.binary_sensor import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.0.7', 'pydispatcher==2.0.5']
|
||||
REQUIREMENTS = ['pyhik==0.1.2']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
@@ -32,7 +33,6 @@ ATTR_DELAY = 'delay'
|
||||
DEVICE_CLASS_MAP = {
|
||||
'Motion': 'motion',
|
||||
'Line Crossing': 'motion',
|
||||
'IO Trigger': None,
|
||||
'Field Detection': 'motion',
|
||||
'Video Loss': None,
|
||||
'Tamper Detection': 'motion',
|
||||
@@ -46,6 +46,7 @@ DEVICE_CLASS_MAP = {
|
||||
'Bad Video': None,
|
||||
'PIR Alarm': 'motion',
|
||||
'Face Detection': 'motion',
|
||||
'Scene Change Detection': 'motion',
|
||||
}
|
||||
|
||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||
@@ -90,24 +91,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
entities = []
|
||||
|
||||
for sensor in data.sensors:
|
||||
# Build sensor name, then parse customize config.
|
||||
sensor_name = sensor.replace(' ', '_')
|
||||
for sensor, channel_list in data.sensors.items():
|
||||
for channel in channel_list:
|
||||
# Build sensor name, then parse customize config.
|
||||
if data.type == 'NVR':
|
||||
sensor_name = '{}_{}'.format(
|
||||
sensor.replace(' ', '_'), channel[1])
|
||||
else:
|
||||
sensor_name = sensor.replace(' ', '_')
|
||||
|
||||
custom = customize.get(sensor_name.lower(), {})
|
||||
ignore = custom.get(CONF_IGNORED)
|
||||
delay = custom.get(CONF_DELAY)
|
||||
custom = customize.get(sensor_name.lower(), {})
|
||||
ignore = custom.get(CONF_IGNORED)
|
||||
delay = custom.get(CONF_DELAY)
|
||||
|
||||
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
|
||||
data.name, sensor_name, ignore, delay)
|
||||
if not ignore:
|
||||
entities.append(HikvisionBinarySensor(hass, sensor, data, delay))
|
||||
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
|
||||
data.name, sensor_name, ignore, delay)
|
||||
if not ignore:
|
||||
entities.append(HikvisionBinarySensor(
|
||||
hass, sensor, channel[1], data, delay))
|
||||
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class HikvisionData(object):
|
||||
"""Hikvision camera event stream object."""
|
||||
"""Hikvision device event stream object."""
|
||||
|
||||
def __init__(self, hass, url, port, name, username, password):
|
||||
"""Initialize the data oject."""
|
||||
@@ -119,49 +126,64 @@ class HikvisionData(object):
|
||||
self._password = password
|
||||
|
||||
# Establish camera
|
||||
self._cam = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
self.camdata = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
|
||||
if self._name is None:
|
||||
self._name = self._cam.get_name
|
||||
|
||||
# Start event stream
|
||||
self._cam.start_stream()
|
||||
self._name = self.camdata.get_name
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_hik)
|
||||
|
||||
def stop_hik(self, event):
|
||||
"""Shutdown Hikvision subscriptions and subscription thread on exit."""
|
||||
self._cam.disconnect()
|
||||
self.camdata.disconnect()
|
||||
|
||||
def start_hik(self, event):
|
||||
"""Start Hikvision event stream thread."""
|
||||
self.camdata.start_stream()
|
||||
|
||||
@property
|
||||
def sensors(self):
|
||||
"""Return list of available sensors and their states."""
|
||||
return self._cam.current_event_states
|
||||
return self.camdata.current_event_states
|
||||
|
||||
@property
|
||||
def cam_id(self):
|
||||
"""Return camera id."""
|
||||
return self._cam.get_id
|
||||
"""Return device id."""
|
||||
return self.camdata.get_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return camera name."""
|
||||
"""Return device name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Return device type."""
|
||||
return self.camdata.get_type
|
||||
|
||||
def get_attributes(self, sensor, channel):
|
||||
"""Return attribute list for sensor/channel."""
|
||||
return self.camdata.fetch_attributes(sensor, channel)
|
||||
|
||||
|
||||
class HikvisionBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Hikvision binary sensor."""
|
||||
|
||||
def __init__(self, hass, sensor, cam, delay):
|
||||
def __init__(self, hass, sensor, channel, cam, delay):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
|
||||
self._hass = hass
|
||||
self._cam = cam
|
||||
self._name = self._cam.name + ' ' + sensor
|
||||
self._id = self._cam.cam_id + '.' + sensor
|
||||
self._sensor = sensor
|
||||
self._channel = channel
|
||||
|
||||
if self._cam.type == 'NVR':
|
||||
self._name = '{} {} {}'.format(self._cam.name, sensor, channel)
|
||||
else:
|
||||
self._name = '{} {}'.format(self._cam.name, sensor)
|
||||
|
||||
self._id = '{}.{}.{}'.format(self._cam.cam_id, sensor, channel)
|
||||
|
||||
if delay is None:
|
||||
self._delay = 0
|
||||
@@ -170,20 +192,16 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
self._timer = None
|
||||
|
||||
# Form signal for dispatcher
|
||||
signal = 'ValueChanged.{}'.format(self._cam.cam_id)
|
||||
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=signal,
|
||||
sender=self._sensor)
|
||||
# Register callback function with pyHik
|
||||
self._cam.camdata.add_update_callback(self._update_callback, self._id)
|
||||
|
||||
def _sensor_state(self):
|
||||
"""Extract sensor state."""
|
||||
return self._cam.sensors[self._sensor][0]
|
||||
return self._cam.get_attributes(self._sensor, self._channel)[0]
|
||||
|
||||
def _sensor_last_update(self):
|
||||
"""Extract sensor last update time."""
|
||||
return self._cam.sensors[self._sensor][3]
|
||||
return self._cam.get_attributes(self._sensor, self._channel)[3]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -225,13 +243,9 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
return attr
|
||||
|
||||
def _update_callback(self, signal, sender):
|
||||
def _update_callback(self, msg):
|
||||
"""Update the sensor's state, if needed."""
|
||||
_LOGGER.debug('Dispatcher callback, signal: %s, sender: %s',
|
||||
signal, sender)
|
||||
|
||||
if sender is not self._sensor:
|
||||
return
|
||||
_LOGGER.debug('Callback signal from: %s', msg)
|
||||
|
||||
if self._delay > 0 and not self.is_on:
|
||||
# Set timer to wait until updating the state
|
||||
|
||||
@@ -32,7 +32,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
InsteonPLMBinarySensorDevice(hass, plm, address, name)
|
||||
)
|
||||
|
||||
hass.async_add_job(async_add_devices(device_list))
|
||||
async_add_devices(device_list)
|
||||
|
||||
|
||||
class InsteonPLMBinarySensorDevice(BinarySensorDevice):
|
||||
|
||||
76
homeassistant/components/binary_sensor/maxcube.py
Normal file
76
homeassistant/components/binary_sensor/maxcube.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Support for MAX! Window Shutter via MAX! Cube.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/maxcube/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add window shutters to HASS."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
|
||||
# List of devices
|
||||
devices = []
|
||||
|
||||
for device in cube.devices:
|
||||
# Create device name by concatenating room name + device name
|
||||
name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
# Only add Window Shutters
|
||||
if cube.is_windowshutter(device):
|
||||
# add device to HASS
|
||||
devices.append(MaxCubeShutter(hass, name, device.rf_address))
|
||||
|
||||
if len(devices) > 0:
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class MaxCubeShutter(BinarySensorDevice):
|
||||
"""MAX! Cube BinarySensor device."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
"""Initialize MAX! Cube BinarySensorDevice."""
|
||||
self._name = name
|
||||
self._sensor_type = 'opening'
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is required."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the BinarySensorDevice."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on/open."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get latest data from MAX! Cube."""
|
||||
self._cubehandle.update()
|
||||
|
||||
# Get the device we want to update
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Update our internal state
|
||||
self._state = device.is_open
|
||||
@@ -46,7 +46,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
yield from async_add_devices([MqttBinarySensor(
|
||||
async_add_devices([MqttBinarySensor(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS),
|
||||
|
||||
130
homeassistant/components/binary_sensor/ping.py
Normal file
130
homeassistant/components/binary_sensor/ping.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Tracks the latency of a host by sending ICMP echo requests (ping).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ping/
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_NAME, CONF_HOST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ROUND_TRIP_TIME_AVG = 'round_trip_time_avg'
|
||||
ATTR_ROUND_TRIP_TIME_MAX = 'round_trip_time_max'
|
||||
ATTR_ROUND_TRIP_TIME_MDEV = 'round_trip_time_mdev'
|
||||
ATTR_ROUND_TRIP_TIME_MIN = 'round_trip_time_min'
|
||||
|
||||
CONF_PING_COUNT = 'count'
|
||||
|
||||
DEFAULT_NAME = 'Ping Binary sensor'
|
||||
DEFAULT_PING_COUNT = 5
|
||||
DEFAULT_SENSOR_CLASS = 'connectivity'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
PING_MATCHER = re.compile(
|
||||
r'(?P<min>\d+.\d+)\/(?P<avg>\d+.\d+)\/(?P<max>\d+.\d+)\/(?P<mdev>\d+.\d+)')
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PING_COUNT, default=DEFAULT_PING_COUNT): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Ping Binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
count = config.get(CONF_PING_COUNT)
|
||||
|
||||
add_devices([PingBinarySensor(name, PingData(host, count))], True)
|
||||
|
||||
|
||||
class PingBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Ping Binary sensor."""
|
||||
|
||||
def __init__(self, name, ping):
|
||||
"""Initialize the Ping Binary sensor."""
|
||||
self._name = name
|
||||
self.ping = ping
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEFAULT_SENSOR_CLASS
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.ping.available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the ICMP checo request."""
|
||||
if self.ping.data is not False:
|
||||
return {
|
||||
ATTR_ROUND_TRIP_TIME_AVG: self.ping.data['avg'],
|
||||
ATTR_ROUND_TRIP_TIME_MAX: self.ping.data['max'],
|
||||
ATTR_ROUND_TRIP_TIME_MDEV: self.ping.data['mdev'],
|
||||
ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'],
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
self.ping.update()
|
||||
|
||||
|
||||
class PingData(object):
|
||||
"""The Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, host, count):
|
||||
"""Initialize the data object."""
|
||||
self._ip_address = host
|
||||
self._count = count
|
||||
self.data = {}
|
||||
self.available = False
|
||||
|
||||
if sys.platform == 'win32':
|
||||
self._ping_cmd = [
|
||||
'ping', '-n', str(self._count), '-w 1000', self._ip_address]
|
||||
else:
|
||||
self._ping_cmd = [
|
||||
'ping', '-n', '-q', '-c', str(self._count), '-W1',
|
||||
self._ip_address]
|
||||
|
||||
def ping(self):
|
||||
"""Send ICMP echo request and return details if success."""
|
||||
pinger = subprocess.Popen(
|
||||
self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
out = pinger.communicate()
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
except (subprocess.CalledProcessError, AttributeError):
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Retrieve the latest details from the host."""
|
||||
self.data = self.ping()
|
||||
self.available = bool(self.data)
|
||||
109
homeassistant/components/binary_sensor/ring.py
Normal file
109
homeassistant/components/binary_sensor/ring.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
This component provides HA sensor support for Ring Door Bell/Chimes.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ring/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.ring import (
|
||||
CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE)
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS)
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
|
||||
DEPENDENCIES = ['ring']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
# Sensor types: Name, category, device_class
|
||||
SENSOR_TYPES = {
|
||||
'ding': ['Ding', ['doorbell'], 'occupancy'],
|
||||
'motion': ['Motion', ['doorbell'], 'motion'],
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE):
|
||||
cv.string,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for a Ring device."""
|
||||
ring = hass.data.get('ring')
|
||||
|
||||
sensors = []
|
||||
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||
for device in ring.doorbells:
|
||||
if 'doorbell' in SENSOR_TYPES[sensor_type][1]:
|
||||
sensors.append(RingBinarySensor(hass,
|
||||
device,
|
||||
sensor_type))
|
||||
add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
class RingBinarySensor(BinarySensorDevice):
|
||||
"""A binary sensor implementation for Ring device."""
|
||||
|
||||
def __init__(self, hass, data, sensor_type):
|
||||
"""Initialize a sensor for Ring device."""
|
||||
super(RingBinarySensor, self).__init__()
|
||||
self._sensor_type = sensor_type
|
||||
self._data = data
|
||||
self._name = "{0} {1}".format(self._data.name,
|
||||
SENSOR_TYPES.get(self._sensor_type)[0])
|
||||
self._device_class = SENSOR_TYPES.get(self._sensor_type)[2]
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {}
|
||||
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
||||
|
||||
attrs['device_id'] = self._data.id
|
||||
attrs['firmware'] = self._data.firmware
|
||||
attrs['timezone'] = self._data.timezone
|
||||
|
||||
if self._data.alert and self._data.alert_expires_at:
|
||||
attrs['expires_at'] = self._data.alert_expires_at
|
||||
attrs['state'] = self._data.alert.get('state')
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
self._data.check_alerts()
|
||||
|
||||
if self._data.alert:
|
||||
self._state = (self._sensor_type ==
|
||||
self._data.alert.get('kind'))
|
||||
else:
|
||||
self._state = False
|
||||
@@ -15,12 +15,14 @@ from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS)
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS,
|
||||
EVENT_HOMEASSISTANT_START, STATE_ON)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -66,7 +68,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
|
||||
yield from async_add_devices(sensors, True)
|
||||
async_add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
@@ -83,14 +85,30 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
self._device_class = device_class
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
self._entities = entity_ids
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._state = state.state == STATE_ON
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
async_track_state_change(
|
||||
hass, entity_ids, template_bsensor_state_listener)
|
||||
@callback
|
||||
def template_bsensor_startup(event):
|
||||
"""Update template on startup."""
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_bsensor_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -52,7 +52,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
limit_type = config.get(CONF_TYPE)
|
||||
device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
|
||||
yield from async_add_devices(
|
||||
async_add_devices(
|
||||
[ThresholdSensor(hass, entity_id, name, threshold, limit_type,
|
||||
device_class)], True)
|
||||
return True
|
||||
|
||||
@@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice)
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@@ -30,6 +30,7 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._state = False
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
import pywemo.discovery as discovery
|
||||
|
||||
if discovery_info is not None:
|
||||
location = discovery_info[2]
|
||||
mac = discovery_info[3]
|
||||
location = discovery_info['ssdp_description']
|
||||
mac = discovery_info['mac_address']
|
||||
device = discovery.device_from_description(location, mac)
|
||||
|
||||
if device:
|
||||
@@ -40,12 +40,14 @@ class WemoBinarySensor(BinarySensorDevice):
|
||||
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
|
||||
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)
|
||||
|
||||
def _update_callback(self, _device, _params):
|
||||
"""Called by the wemo device callback to update state."""
|
||||
def _update_callback(self, _device, _type, _params):
|
||||
"""Called by the Wemo device callback to update state."""
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s',
|
||||
_device)
|
||||
self.update()
|
||||
updated = self.wemo.subscription_update(_type, _params)
|
||||
self._update(force_update=(not updated))
|
||||
|
||||
if not hasattr(self, 'hass'):
|
||||
return
|
||||
self.schedule_update_ha_state()
|
||||
@@ -72,7 +74,11 @@ class WemoBinarySensor(BinarySensorDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update WeMo state."""
|
||||
self._update(force_update=True)
|
||||
|
||||
def _update(self, force_update=True):
|
||||
try:
|
||||
self._state = self.wemo.get_state(True)
|
||||
except AttributeError:
|
||||
_LOGGER.warning('Could not update status for %s', self.name)
|
||||
self._state = self.wemo.get_state(force_update)
|
||||
except AttributeError as err:
|
||||
_LOGGER.warning('Could not update status for %s (%s)',
|
||||
self.name, err)
|
||||
|
||||
148
homeassistant/components/binary_sensor/workday.py
Normal file
148
homeassistant/components/binary_sensor/workday.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
Sensor to indicate whether the current day is a workday.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.workday/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_NAME, WEEKDAYS)
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.8.1']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Canada', 'CA',
|
||||
'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'England',
|
||||
'EuropeanCentralBank', 'ECB', 'TAR', 'Germany', 'DE',
|
||||
'Ireland', 'Isle of Man', 'Mexico', 'MX', 'Netherlands', 'NL',
|
||||
'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO',
|
||||
'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Spain',
|
||||
'ES', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales']
|
||||
CONF_COUNTRY = 'country'
|
||||
CONF_PROVINCE = 'province'
|
||||
CONF_WORKDAYS = 'workdays'
|
||||
# By default, Monday - Friday are workdays
|
||||
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
|
||||
CONF_EXCLUDES = 'excludes'
|
||||
# By default, public holidays, Saturdays and Sundays are excluded from workdays
|
||||
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
|
||||
DEFAULT_NAME = 'Workday Sensor'
|
||||
ALLOWED_DAYS = WEEKDAYS + ['holiday']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
|
||||
vol.Optional(CONF_PROVINCE, default=None): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Workday sensor."""
|
||||
import holidays
|
||||
|
||||
sensor_name = config.get(CONF_NAME)
|
||||
country = config.get(CONF_COUNTRY)
|
||||
province = config.get(CONF_PROVINCE)
|
||||
workdays = config.get(CONF_WORKDAYS)
|
||||
excludes = config.get(CONF_EXCLUDES)
|
||||
|
||||
year = datetime.datetime.now().year
|
||||
obj_holidays = getattr(holidays, country)(years=year)
|
||||
|
||||
if province:
|
||||
if province not in obj_holidays.PROVINCES and \
|
||||
province not in obj_holidays.STATES:
|
||||
_LOGGER.error("There is no province/state %s in country %s",
|
||||
province, country)
|
||||
return False
|
||||
else:
|
||||
year = datetime.datetime.now().year
|
||||
obj_holidays = getattr(holidays, country)(prov=province,
|
||||
years=year)
|
||||
|
||||
_LOGGER.debug("Found the following holidays for your configuration:")
|
||||
for date, name in sorted(obj_holidays.items()):
|
||||
_LOGGER.debug("%s %s", date, name)
|
||||
|
||||
add_devices([IsWorkdaySensor(
|
||||
obj_holidays, workdays, excludes, sensor_name)], True)
|
||||
|
||||
|
||||
def day_to_string(day):
|
||||
"""Convert day index 0 - 7 to string."""
|
||||
try:
|
||||
return ALLOWED_DAYS[day]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
class IsWorkdaySensor(Entity):
|
||||
"""Implementation of a Workday sensor."""
|
||||
|
||||
def __init__(self, obj_holidays, workdays, excludes, name):
|
||||
"""Initialize the Workday sensor."""
|
||||
self._name = name
|
||||
self._obj_holidays = obj_holidays
|
||||
self._workdays = workdays
|
||||
self._excludes = excludes
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def is_include(self, day, now):
|
||||
"""Check if given day is in the includes list."""
|
||||
if day in self._workdays:
|
||||
return True
|
||||
elif 'holiday' in self._workdays and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_exclude(self, day, now):
|
||||
"""Check if given day is in the excludes list."""
|
||||
if day in self._excludes:
|
||||
return True
|
||||
elif 'holiday' in self._excludes and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get date and look whether it is a holiday."""
|
||||
# Default is no workday
|
||||
self._state = STATE_OFF
|
||||
|
||||
# Get iso day of the week (1 = Monday, 7 = Sunday)
|
||||
day = datetime.datetime.today().isoweekday() - 1
|
||||
day_of_week = day_to_string(day)
|
||||
|
||||
if self.is_include(day_of_week, dt_util.now()):
|
||||
self._state = STATE_ON
|
||||
|
||||
if self.is_exclude(day_of_week, dt_util.now()):
|
||||
self._state = STATE_OFF
|
||||
@@ -24,7 +24,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the ZigBee binary sensor platform."""
|
||||
add_devices([ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))])
|
||||
add_devices(
|
||||
[ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))], True)
|
||||
|
||||
|
||||
class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice):
|
||||
|
||||
@@ -10,6 +10,7 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import workaround
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
BinarySensorDevice)
|
||||
@@ -18,45 +19,34 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Z-Wave platform for binary sensors."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
|
||||
device_mapping = workaround.get_device_mapping(value)
|
||||
def get_device(values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
device_mapping = workaround.get_device_mapping(values.primary)
|
||||
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
|
||||
# Default the multiplier to 4
|
||||
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
|
||||
add_devices([
|
||||
ZWaveTriggerSensor(value, "motion",
|
||||
hass, re_arm_multiplier * 8)
|
||||
])
|
||||
return
|
||||
re_arm_multiplier = zwave.get_config_value(values.primary.node, 9) or 4
|
||||
return ZWaveTriggerSensor(values, "motion", re_arm_multiplier * 8)
|
||||
|
||||
if workaround.get_device_component_mapping(value) == DOMAIN:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
return
|
||||
if workaround.get_device_component_mapping(values.primary) == DOMAIN:
|
||||
return ZWaveBinarySensor(values, None)
|
||||
|
||||
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
if values.primary.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
return ZWaveBinarySensor(values, None)
|
||||
return None
|
||||
|
||||
|
||||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
"""Representation of a binary sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, device_class):
|
||||
def __init__(self, values, device_class):
|
||||
"""Initialize the sensor."""
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._sensor_type = device_class
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -68,35 +58,27 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
"""Representation of a stateless sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, device_class, hass, re_arm_sec=60):
|
||||
def __init__(self, values, device_class, re_arm_sec=60):
|
||||
"""Initialize the sensor."""
|
||||
super(ZWaveTriggerSensor, self).__init__(value, device_class)
|
||||
self._hass = hass
|
||||
super(ZWaveTriggerSensor, self).__init__(values, device_class)
|
||||
self.re_arm_sec = re_arm_sec
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
# If it's active make sure that we set the timeout tracker
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
self.invalidate_after = None
|
||||
|
||||
def update_properties(self):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
# only allow this value to be true for re_arm secs
|
||||
if not self.hass:
|
||||
return
|
||||
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@property
|
||||
|
||||
89
homeassistant/components/blink.py
Normal file
89
homeassistant/components/blink.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Support for Blink Home Camera System.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/blink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED)
|
||||
from homeassistant.helpers import discovery
|
||||
|
||||
REQUIREMENTS = ['blinkpy==0.5.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'blink'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ARM_SYSTEM_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ARMED): cv.boolean
|
||||
})
|
||||
|
||||
ARM_CAMERA_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(ATTR_ARMED): cv.boolean
|
||||
})
|
||||
|
||||
SNAP_PICTURE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_FRIENDLY_NAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
class BlinkSystem(object):
|
||||
"""Blink System class."""
|
||||
|
||||
def __init__(self, config_info):
|
||||
"""Initialize the system."""
|
||||
import blinkpy
|
||||
self.blink = blinkpy.Blink(username=config_info[DOMAIN][CONF_USERNAME],
|
||||
password=config_info[DOMAIN][CONF_PASSWORD])
|
||||
self.blink.setup_system()
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up Blink System."""
|
||||
hass.data[DOMAIN] = BlinkSystem(config)
|
||||
discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
||||
|
||||
def snap_picture(call):
|
||||
"""Take a picture."""
|
||||
cameras = hass.data[DOMAIN].blink.cameras
|
||||
name = call.data.get(ATTR_FRIENDLY_NAME, '')
|
||||
if name in cameras:
|
||||
cameras[name].snap_picture()
|
||||
|
||||
def arm_camera(call):
|
||||
"""Arm a camera."""
|
||||
cameras = hass.data[DOMAIN].blink.cameras
|
||||
name = call.data.get(ATTR_FRIENDLY_NAME, '')
|
||||
value = call.data.get(ATTR_ARMED, True)
|
||||
if name in cameras:
|
||||
cameras[name].set_motion_detect(value)
|
||||
|
||||
def arm_system(call):
|
||||
"""Arm the system."""
|
||||
value = call.data.get(ATTR_ARMED, True)
|
||||
hass.data[DOMAIN].blink.arm = value
|
||||
hass.data[DOMAIN].blink.refresh()
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, 'snap_picture', snap_picture, schema=SNAP_PICTURE_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN, 'arm_camera', arm_camera, schema=ARM_CAMERA_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN, 'arm_system', arm_system, schema=ARM_SYSTEM_SCHEMA)
|
||||
|
||||
return True
|
||||
@@ -3,8 +3,8 @@ Support for Google Calendar event device sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -27,13 +27,13 @@ DOMAIN = 'calendar'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ class CalendarEventDevice(Entity):
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
||||
summary = self.data.event['summary']
|
||||
summary = self.data.event.get('summary', '')
|
||||
|
||||
# check if we have an offset tag in the message
|
||||
# time is HH:MM or MM
|
||||
|
||||
@@ -7,6 +7,7 @@ https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import asyncio
|
||||
import collections
|
||||
from contextlib import suppress
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
@@ -58,7 +59,6 @@ def async_get_image(hass, entity_id, timeout=10):
|
||||
state.attributes.get(ATTR_ENTITY_PICTURE)
|
||||
)
|
||||
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
response = yield from websession.get(url)
|
||||
@@ -70,13 +70,9 @@ def async_get_image(hass, entity_id, timeout=10):
|
||||
image = yield from response.read()
|
||||
return image
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
raise HomeAssistantError("Can't connect to {0}".format(url))
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
@@ -172,7 +168,7 @@ class Camera(Entity):
|
||||
if not img_bytes:
|
||||
break
|
||||
|
||||
if img_bytes is not None and img_bytes != last_image:
|
||||
if img_bytes and img_bytes != last_image:
|
||||
write(img_bytes)
|
||||
|
||||
# Chrome seems to always ignore first picture,
|
||||
@@ -185,8 +181,8 @@ class Camera(Entity):
|
||||
|
||||
yield from asyncio.sleep(.5)
|
||||
|
||||
except (asyncio.CancelledError, ConnectionResetError):
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
@@ -268,16 +264,14 @@ class CameraImageView(CameraView):
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
try:
|
||||
image = yield from camera.async_camera_image()
|
||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||
with async_timeout.timeout(10, loop=request.app['hass'].loop):
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
if image is None:
|
||||
return web.Response(status=500)
|
||||
if image:
|
||||
return web.Response(body=image)
|
||||
|
||||
return web.Response(body=image)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
return web.Response(status=500)
|
||||
|
||||
|
||||
class CameraMjpegStream(CameraView):
|
||||
|
||||
@@ -16,9 +16,9 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_stream)
|
||||
async_get_clientsession, async_aiohttp_proxy_web)
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.1.4']
|
||||
REQUIREMENTS = ['amcrest==1.1.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -125,7 +125,7 @@ class AmcrestCam(Camera):
|
||||
stream_coro = websession.get(
|
||||
streaming_url, auth=self._token, timeout=TIMEOUT)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
|
||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
81
homeassistant/components/camera/blink.py
Normal file
81
homeassistant/components/camera/blink.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Support for Blink system camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.blink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
|
||||
from homeassistant.components.blink import DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup a Blink Camera."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[DOMAIN].blink
|
||||
devs = list()
|
||||
for name in data.cameras:
|
||||
devs.append(BlinkCamera(hass, config, data, name))
|
||||
|
||||
add_devices(devs)
|
||||
|
||||
|
||||
class BlinkCamera(Camera):
|
||||
"""An implementation of a Blink Camera."""
|
||||
|
||||
def __init__(self, hass, config, data, name):
|
||||
"""Initialize a camera."""
|
||||
super().__init__()
|
||||
self.data = data
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.notifications = self.data.cameras[self._name].notifications
|
||||
self.response = None
|
||||
|
||||
_LOGGER.info("Initialized blink camera %s", self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""A camera name."""
|
||||
return self._name
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def request_image(self):
|
||||
"""An image request from Blink servers."""
|
||||
_LOGGER.info("Requesting new image from blink servers")
|
||||
image_url = self.check_for_motion()
|
||||
header = self.data.cameras[self._name].header
|
||||
self.response = requests.get(image_url, headers=header, stream=True)
|
||||
|
||||
def check_for_motion(self):
|
||||
"""A method to check if motion has been detected since last update."""
|
||||
self.data.refresh()
|
||||
notifs = self.data.cameras[self._name].notifications
|
||||
if notifs > self.notifications:
|
||||
# We detected motion at some point
|
||||
self.data.last_motion()
|
||||
self.notifications = notifs
|
||||
# returning motion image currently not working
|
||||
# return self.data.cameras[self._name].motion['image']
|
||||
elif notifs < self.notifications:
|
||||
self.notifications = notifs
|
||||
|
||||
return self.data.camera_thumbs[self._name]
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
self.request_image()
|
||||
return self.response.content
|
||||
@@ -8,14 +8,14 @@ import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import (
|
||||
DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream)
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -34,7 +34,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
yield from async_add_devices([FFmpegCamera(hass, config)])
|
||||
async_add_devices([FFmpegCamera(hass, config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
@@ -69,26 +69,10 @@ class FFmpegCamera(Camera):
|
||||
yield from stream.open_camera(
|
||||
self._input, extra_cmd=self._extra_arguments)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = 'multipart/x-mixed-replace;boundary=ffserver'
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Close stream by frontend.")
|
||||
response = None
|
||||
|
||||
finally:
|
||||
yield from stream.close()
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -47,22 +47,32 @@ class FoscamCamera(Camera):
|
||||
port = device_info.get(CONF_PORT)
|
||||
|
||||
self._base_url = 'http://{}:{}/'.format(ip_address, port)
|
||||
|
||||
uri_template = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?' \
|
||||
+ 'cmd=snapPicture2&usr={}&pwd={}'
|
||||
|
||||
self._username = device_info.get(CONF_USERNAME)
|
||||
self._password = device_info.get(CONF_PASSWORD)
|
||||
self._snap_picture_url = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \
|
||||
+ self._username + '&pwd=' + self._password
|
||||
self._snap_picture_url = uri_template.format(
|
||||
self._username,
|
||||
self._password
|
||||
)
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
|
||||
_LOGGER.info('Using the following URL for %s: %s',
|
||||
self._name, self._snap_picture_url)
|
||||
self._name, uri_template.format('***', '***'))
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = requests.get(self._snap_picture_url, timeout=10)
|
||||
|
||||
return response.content
|
||||
# Handle exception if host is not reachable or url failed
|
||||
try:
|
||||
response = requests.get(self._snap_picture_url, timeout=10)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return None
|
||||
else:
|
||||
return response.content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a generic IP Camera."""
|
||||
yield from async_add_devices([GenericCamera(hass, config)])
|
||||
async_add_devices([GenericCamera(hass, config)])
|
||||
|
||||
|
||||
class GenericCamera(Camera):
|
||||
@@ -107,7 +107,6 @@ class GenericCamera(Camera):
|
||||
None, fetch)
|
||||
# async
|
||||
else:
|
||||
response = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
@@ -117,14 +116,9 @@ class GenericCamera(Camera):
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error('Timeout getting camera image')
|
||||
return self._last_image
|
||||
except (aiohttp.errors.ClientError,
|
||||
aiohttp.errors.DisconnectedError,
|
||||
aiohttp.errors.HttpProcessingError) as err:
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error('Error getting new camera image: %s', err)
|
||||
return self._last_image
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
self._last_url = url
|
||||
return self._last_image
|
||||
|
||||
@@ -20,7 +20,7 @@ CONF_FILE_PATH = 'file_path'
|
||||
DEFAULT_NAME = 'Local File'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_FILE_PATH): cv.isfile,
|
||||
vol.Required(CONF_FILE_PATH): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
@@ -31,8 +31,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# check filepath given is readable
|
||||
if not os.access(file_path, os.R_OK):
|
||||
_LOGGER.error("file path is not readable")
|
||||
return False
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
config[CONF_NAME], file_path)
|
||||
|
||||
add_devices([LocalFile(config[CONF_NAME], file_path)])
|
||||
|
||||
@@ -49,8 +49,12 @@ class LocalFile(Camera):
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
with open(self._file_path, 'rb') as file:
|
||||
return file.read()
|
||||
try:
|
||||
with open(self._file_path, 'rb') as file:
|
||||
return file.read()
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
self._name, self._file_path)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_stream)
|
||||
async_get_clientsession, async_aiohttp_proxy_web)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -45,7 +45,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a MJPEG IP Camera."""
|
||||
yield from async_add_devices([MjpegCamera(hass, config)])
|
||||
if discovery_info:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
async_add_devices([MjpegCamera(hass, config)])
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
@@ -91,7 +93,6 @@ class MjpegCamera(Camera):
|
||||
return image
|
||||
|
||||
websession = async_get_clientsession(self.hass)
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from websession.get(
|
||||
@@ -103,14 +104,9 @@ class MjpegCamera(Camera):
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error('Timeout getting camera image')
|
||||
|
||||
except (aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError) as err:
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error('Error getting new camera image: %s', err)
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
if self._username and self._password:
|
||||
@@ -138,7 +134,7 @@ class MjpegCamera(Camera):
|
||||
websession = async_get_clientsession(self.hass)
|
||||
stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
|
||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
74
homeassistant/components/camera/mqtt.py
Executable file
74
homeassistant/components/camera/mqtt.py
Executable file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Camera that loads a picture from an MQTT topic.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.mqtt/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TOPIC = 'topic'
|
||||
|
||||
DEFAULT_NAME = 'MQTT Camera'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the Camera."""
|
||||
topic = config[CONF_TOPIC]
|
||||
|
||||
async_add_devices([MqttCamera(config[CONF_NAME], topic)])
|
||||
|
||||
|
||||
class MqttCamera(Camera):
|
||||
"""MQTT camera."""
|
||||
|
||||
def __init__(self, name, topic):
|
||||
"""Initialize Local File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._qos = 0
|
||||
self._last_image = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return image response."""
|
||||
return self._last_image
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
self._last_image = payload
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._topic, message_received, self._qos, None)
|
||||
65
homeassistant/components/camera/neato.py
Normal file
65
homeassistant/components/camera/neato.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Camera that loads a picture from a local file.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.neato/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.neato import (
|
||||
NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN)
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['neato']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Camera."""
|
||||
dev = []
|
||||
for robot in hass.data[NEATO_ROBOTS]:
|
||||
if 'maps' in robot.traits:
|
||||
dev.append(NeatoCleaningMap(hass, robot))
|
||||
_LOGGER.debug('Adding robots for cleaning maps %s', dev)
|
||||
add_devices(dev, True)
|
||||
|
||||
|
||||
class NeatoCleaningMap(Camera):
|
||||
"""Neato cleaning map for last clean."""
|
||||
|
||||
def __init__(self, hass, robot):
|
||||
"""Initialize Neato cleaning map."""
|
||||
super().__init__()
|
||||
self.robot = robot
|
||||
self._robot_name = self.robot.name + ' Cleaning Map'
|
||||
self._robot_serial = self.robot.serial
|
||||
self.neato = hass.data[NEATO_LOGIN]
|
||||
self._image_url = None
|
||||
self._image = None
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
self.update()
|
||||
return self._image
|
||||
|
||||
@Throttle(timedelta(seconds=10))
|
||||
def update(self):
|
||||
"""Check the contents of the map list."""
|
||||
self.neato.update_robots()
|
||||
image_url = None
|
||||
map_data = self.hass.data[NEATO_MAP_DATA]
|
||||
image_url = map_data[self._robot_serial]['maps'][0]['url']
|
||||
if image_url == self._image_url:
|
||||
_LOGGER.debug('The map image_url is the same as old')
|
||||
return
|
||||
image = self.neato.download_map(image_url)
|
||||
self._image = image.read()
|
||||
self._image_url = image_url
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._robot_name
|
||||
@@ -14,12 +14,12 @@ import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_create_clientsession,
|
||||
async_aiohttp_proxy_stream)
|
||||
async_aiohttp_proxy_web)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
@@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
TIMEOUT = 5
|
||||
DEFAULT_TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
|
||||
@@ -51,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
})
|
||||
@@ -60,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||
|
||||
# Determine API to use for authentication
|
||||
@@ -72,28 +74,25 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
'version': '1',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
query_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
)
|
||||
|
||||
query_resp = yield from query_req.json()
|
||||
# Skip content type check because Synology doesn't return JSON with
|
||||
# right content type
|
||||
query_resp = yield from query_req.json(content_type=None)
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_api_url)
|
||||
return False
|
||||
|
||||
finally:
|
||||
if query_req is not None:
|
||||
yield from query_req.release()
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||
@@ -103,7 +102,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
websession_init,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url
|
||||
syno_auth_url,
|
||||
timeout
|
||||
)
|
||||
|
||||
# init websession
|
||||
@@ -120,18 +120,17 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
camera_req = yield from websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_camera_url)
|
||||
return False
|
||||
|
||||
camera_resp = yield from camera_req.json()
|
||||
camera_resp = yield from camera_req.json(content_type=None)
|
||||
cameras = camera_resp['data']['cameras']
|
||||
yield from camera_req.release()
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
@@ -149,15 +148,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_path
|
||||
auth_path,
|
||||
timeout
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, websession, username, password, login_url):
|
||||
def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||
"""Get a session id."""
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
@@ -168,31 +168,26 @@ def get_session_id(hass, websession, username, password, login_url):
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
auth_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
)
|
||||
auth_resp = yield from auth_req.json()
|
||||
auth_resp = yield from auth_req.json(content_type=None)
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", login_url)
|
||||
return False
|
||||
|
||||
finally:
|
||||
if auth_req is not None:
|
||||
yield from auth_req.release()
|
||||
|
||||
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
||||
def __init__(self, hass, websession, config, camera_id,
|
||||
camera_name, snapshot_path, streaming_path, camera_path,
|
||||
auth_path):
|
||||
auth_path, timeout):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
@@ -206,6 +201,7 @@ class SynologyCamera(Camera):
|
||||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
self._timeout = timeout
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
@@ -225,17 +221,16 @@ class SynologyCamera(Camera):
|
||||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||
response = yield from self._websession.get(
|
||||
image_url,
|
||||
params=image_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", image_url)
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Error fetching %s", image_url)
|
||||
return None
|
||||
|
||||
image = yield from response.read()
|
||||
yield from response.release()
|
||||
|
||||
return image
|
||||
|
||||
@@ -255,7 +250,7 @@ class SynologyCamera(Camera):
|
||||
stream_coro = self._websession.get(
|
||||
streaming_url, params=streaming_payload)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(self.hass, request, stream_coro)
|
||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -19,6 +19,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['zoneminder']
|
||||
DOMAIN = 'zoneminder'
|
||||
|
||||
# From ZoneMinder's web/includes/config.php.in
|
||||
ZM_STATE_ALARM = "2"
|
||||
|
||||
|
||||
def _get_image_url(hass, monitor, mode):
|
||||
zm_data = hass.data[DOMAIN]
|
||||
@@ -69,10 +72,49 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
|
||||
}
|
||||
cameras.append(MjpegCamera(hass, device_info))
|
||||
cameras.append(ZoneMinderCamera(hass, device_info, monitor))
|
||||
|
||||
if not cameras:
|
||||
_LOGGER.warning('No active cameras found')
|
||||
return
|
||||
|
||||
yield from async_add_devices(cameras)
|
||||
async_add_devices(cameras)
|
||||
|
||||
|
||||
class ZoneMinderCamera(MjpegCamera):
|
||||
"""Representation of a ZoneMinder Monitor Stream."""
|
||||
|
||||
def __init__(self, hass, device_info, monitor):
|
||||
"""Initialize as a subclass of MjpegCamera."""
|
||||
super().__init__(hass, device_info)
|
||||
self._monitor_id = int(monitor['Id'])
|
||||
self._is_recording = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Update the recording state periodically."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Update our recording state from the ZM API."""
|
||||
_LOGGER.debug('Updating camera state for monitor %i', self._monitor_id)
|
||||
status_response = zoneminder.get_state(
|
||||
'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
|
||||
)
|
||||
|
||||
if not status_response:
|
||||
_LOGGER.warning('Could not get status for monitor %i',
|
||||
self._monitor_id)
|
||||
return
|
||||
|
||||
if status_response['success'] is False:
|
||||
_LOGGER.warning('Alarm status API call failed for monitor %i',
|
||||
self._monitor_id)
|
||||
return
|
||||
|
||||
self._is_recording = status_response['status'] == ZM_STATE_ALARM
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return whether the monitor is in alarm mode."""
|
||||
return self._is_recording
|
||||
|
||||
@@ -224,7 +224,7 @@ def async_setup(hass, config):
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
climate.async_update_ha_state(True))
|
||||
if hasattr(climate, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
@@ -692,18 +692,16 @@ class ClimateDevice(Entity):
|
||||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if (temp is None or not isinstance(temp, Number) or
|
||||
self.temperature_unit == self.unit_of_measurement):
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
return temp
|
||||
|
||||
value = convert_temperature(temp, self.temperature_unit,
|
||||
self.unit_of_measurement)
|
||||
|
||||
if self.temperature_unit != self.unit_of_measurement:
|
||||
temp = convert_temperature(temp, self.temperature_unit,
|
||||
self.unit_of_measurement)
|
||||
# Round in the units appropriate
|
||||
if self.precision == PRECISION_HALVES:
|
||||
return round(value * 2) / 2.0
|
||||
return round(temp * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(value, 1)
|
||||
return round(temp, 1)
|
||||
else:
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(value)
|
||||
return round(temp)
|
||||
|
||||
@@ -25,6 +25,8 @@ ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time'
|
||||
ATTR_RESUME_ALL = 'resume_all'
|
||||
|
||||
DEFAULT_RESUME_ALL = False
|
||||
TEMPERATURE_HOLD = 'temp'
|
||||
VACATION_HOLD = 'vacation'
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
|
||||
@@ -112,6 +114,8 @@ class Thermostat(ClimateDevice):
|
||||
self.thermostat_index)
|
||||
self._name = self.thermostat['name']
|
||||
self.hold_temp = hold_temp
|
||||
self.vacation = None
|
||||
self._climate_list = self.climate_list
|
||||
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
|
||||
'heat', 'off']
|
||||
self.update_without_throttle = False
|
||||
@@ -187,29 +191,30 @@ class Thermostat(ClimateDevice):
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
events = self.thermostat['events']
|
||||
if any((event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) <= 1)
|
||||
or event['type'] == 'autoAway'
|
||||
for event in events):
|
||||
# away hold is auto away or a temporary hold from away climate
|
||||
hold = 'away'
|
||||
elif any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events):
|
||||
# a permanent away is not considered a hold, but away_mode
|
||||
hold = None
|
||||
elif any(event['holdClimateRef'] == 'home' or
|
||||
event['type'] == 'autoHome'
|
||||
for event in events):
|
||||
# home mode is auto home or any home hold
|
||||
hold = 'home'
|
||||
elif any(event['type'] == 'hold' and event['running']
|
||||
for event in events):
|
||||
hold = 'temp'
|
||||
# temperature hold is any other hold not based on climate
|
||||
else:
|
||||
hold = None
|
||||
return hold
|
||||
for event in events:
|
||||
if event['running']:
|
||||
if event['type'] == 'hold':
|
||||
if event['holdClimateRef'] == 'away':
|
||||
if int(event['endDate'][0:4]) - \
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# a temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
else:
|
||||
# a premanent hold from away climate is away_mode
|
||||
return None
|
||||
elif event['holdClimateRef'] != "":
|
||||
# any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
else:
|
||||
# any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
elif event['type'].startswith('auto'):
|
||||
# all auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
elif event['type'] == 'vacation':
|
||||
self.vacation = event['name']
|
||||
return VACATION_HOLD
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
@@ -232,8 +237,11 @@ class Thermostat(ClimateDevice):
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
"""Return current mode ie. home, away, sleep."""
|
||||
return self.thermostat['program']['currentClimateRef']
|
||||
"""Return current mode, as the user-visible name."""
|
||||
cur = self.thermostat['program']['currentClimateRef']
|
||||
climates = self.thermostat['program']['climates']
|
||||
current = list(filter(lambda x: x['climateRef'] == cur, climates))
|
||||
return current[0]['name']
|
||||
|
||||
@property
|
||||
def fan_min_on_time(self):
|
||||
@@ -261,52 +269,44 @@ class Thermostat(ClimateDevice):
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"operation": operation,
|
||||
"climate_list": self.climate_list,
|
||||
"fan_min_on_time": self.fan_min_on_time
|
||||
}
|
||||
|
||||
def is_vacation_on(self):
|
||||
"""Return true if vacation mode is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['type'] == 'vacation' and event['running']
|
||||
for event in events)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events)
|
||||
return self.current_hold_mode == 'away'
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", 'indefinite')
|
||||
self.update_without_throttle = True
|
||||
self.set_hold_mode('away')
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
self.set_hold_mode(None)
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Set hold mode (away, home, temp)."""
|
||||
"""Set hold mode (away, home, temp, sleep, etc.)."""
|
||||
hold = self.current_hold_mode
|
||||
|
||||
if hold == hold_mode:
|
||||
# no change, so no action required
|
||||
return
|
||||
elif hold_mode == 'away':
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", self.hold_preference())
|
||||
elif hold_mode == 'home':
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"home", self.hold_preference())
|
||||
elif hold_mode == 'temp':
|
||||
self.set_temp_hold(int(self.current_temperature))
|
||||
elif hold_mode == 'None' or hold_mode is None:
|
||||
if hold == VACATION_HOLD:
|
||||
self.data.ecobee.delete_vacation(self.thermostat_index,
|
||||
self.vacation)
|
||||
else:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
else:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
if hold_mode == TEMPERATURE_HOLD:
|
||||
self.set_temp_hold(int(self.current_temperature))
|
||||
else:
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
hold_mode,
|
||||
self.hold_preference())
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_auto_temp_hold(self, heat_temp, cool_temp):
|
||||
"""Set temperature hold in auto mode."""
|
||||
@@ -382,3 +382,9 @@ class Thermostat(ClimateDevice):
|
||||
# as an indefinite away hold is interpreted as away_mode
|
||||
else:
|
||||
return 'nextTransition'
|
||||
|
||||
@property
|
||||
def climate_list(self):
|
||||
"""Return the list of climates currently available."""
|
||||
climates = self.thermostat['program']['climates']
|
||||
return list(map((lambda x: x['name']), climates))
|
||||
|
||||
@@ -16,7 +16,8 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_time_interval)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,6 +36,7 @@ CONF_TARGET_TEMP = 'target_temp'
|
||||
CONF_AC_MODE = 'ac_mode'
|
||||
CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_TOLERANCE = 'tolerance'
|
||||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -47,6 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_KEEP_ALIVE): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
})
|
||||
|
||||
|
||||
@@ -62,10 +66,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
ac_mode = config.get(CONF_AC_MODE)
|
||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
tolerance = config.get(CONF_TOLERANCE)
|
||||
keep_alive = config.get(CONF_KEEP_ALIVE)
|
||||
|
||||
yield from async_add_devices([GenericThermostat(
|
||||
async_add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance)])
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance, keep_alive)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
@@ -73,7 +78,7 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
tolerance):
|
||||
tolerance, keep_alive):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@@ -81,6 +86,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self.ac_mode = ac_mode
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
self._keep_alive = keep_alive
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@@ -94,6 +100,10 @@ class GenericThermostat(ClimateDevice):
|
||||
async_track_state_change(
|
||||
hass, heater_entity_id, self._async_switch_changed)
|
||||
|
||||
if self._keep_alive:
|
||||
async_track_time_interval(
|
||||
hass, self._async_keep_alive, self._keep_alive)
|
||||
|
||||
sensor_state = hass.states.get(sensor_entity_id)
|
||||
if sensor_state:
|
||||
self._async_update_temp(sensor_state)
|
||||
@@ -180,6 +190,14 @@ class GenericThermostat(ClimateDevice):
|
||||
return
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@callback
|
||||
def _async_keep_alive(self, time):
|
||||
"""Called at constant intervals for keep-alive purposes."""
|
||||
if self.current_operation in [STATE_COOL, STATE_HEAT]:
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
|
||||
@callback
|
||||
def _async_update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
|
||||
@@ -16,11 +16,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_MANUAL = "manual"
|
||||
STATE_BOOST = "boost"
|
||||
STATE_COMFORT = "comfort"
|
||||
STATE_LOWERING = "lowering"
|
||||
|
||||
HM_STATE_MAP = {
|
||||
"AUTO_MODE": STATE_AUTO,
|
||||
"MANU_MODE": STATE_MANUAL,
|
||||
"BOOST_MODE": STATE_BOOST,
|
||||
"COMFORT_MODE": STATE_COMFORT,
|
||||
"LOWERING_MODE": STATE_LOWERING
|
||||
}
|
||||
|
||||
HM_TEMP_MAP = [
|
||||
|
||||
@@ -6,10 +6,15 @@ https://home-assistant.io/components/climate.honeywell/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA,
|
||||
ATTR_FAN_MODE, ATTR_FAN_LIST,
|
||||
ATTR_OPERATION_MODE,
|
||||
ATTR_OPERATION_LIST)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE)
|
||||
@@ -21,27 +26,35 @@ REQUIREMENTS = ['evohomeclient==0.2.5',
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
ATTR_FANMODE = 'fanmode'
|
||||
ATTR_SYSTEM_MODE = 'system_mode'
|
||||
ATTR_CURRENT_OPERATION = 'equipment_output_status'
|
||||
|
||||
CONF_AWAY_TEMPERATURE = 'away_temperature'
|
||||
CONF_COOL_AWAY_TEMPERATURE = 'away_cool_temperature'
|
||||
CONF_HEAT_AWAY_TEMPERATURE = 'away_heat_temperature'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE = 16
|
||||
DEFAULT_COOL_AWAY_TEMPERATURE = 30
|
||||
DEFAULT_HEAT_AWAY_TEMPERATURE = 16
|
||||
DEFAULT_REGION = 'eu'
|
||||
REGIONS = ['eu', 'us']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE, default=DEFAULT_AWAY_TEMPERATURE):
|
||||
vol.Coerce(float),
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE,
|
||||
default=DEFAULT_AWAY_TEMPERATURE): vol.Coerce(float),
|
||||
vol.Optional(CONF_COOL_AWAY_TEMPERATURE,
|
||||
default=DEFAULT_COOL_AWAY_TEMPERATURE): vol.Coerce(float),
|
||||
vol.Optional(CONF_HEAT_AWAY_TEMPERATURE,
|
||||
default=DEFAULT_HEAT_AWAY_TEMPERATURE): vol.Coerce(float),
|
||||
vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the HoneywelL thermostat."""
|
||||
"""Setup the Honeywell thermostat."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
region = config.get(CONF_REGION)
|
||||
@@ -88,8 +101,11 @@ def _setup_us(username, password, config, add_devices):
|
||||
|
||||
dev_id = config.get('thermostat')
|
||||
loc_id = config.get('location')
|
||||
cool_away_temp = config.get(CONF_COOL_AWAY_TEMPERATURE)
|
||||
heat_away_temp = config.get(CONF_HEAT_AWAY_TEMPERATURE)
|
||||
|
||||
add_devices([HoneywellUSThermostat(client, device)
|
||||
add_devices([HoneywellUSThermostat(client, device, cool_away_temp,
|
||||
heat_away_temp, username, password)
|
||||
for location in client.locations_by_id.values()
|
||||
for device in location.devices_by_id.values()
|
||||
if ((not loc_id or location.locationid == loc_id) and
|
||||
@@ -160,7 +176,7 @@ class RoundThermostat(ClimateDevice):
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on.
|
||||
|
||||
Evohome does have a proprietary away mode, but it doesn't really work
|
||||
Honeywell does have a proprietary away mode, but it doesn't really work
|
||||
the way it should. For example: If you set a temperature manually
|
||||
it doesn't get overwritten when away mode is switched on.
|
||||
"""
|
||||
@@ -199,10 +215,16 @@ class RoundThermostat(ClimateDevice):
|
||||
class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Representation of a Honeywell US Thermostat."""
|
||||
|
||||
def __init__(self, client, device):
|
||||
def __init__(self, client, device, cool_away_temp,
|
||||
heat_away_temp, username, password):
|
||||
"""Initialize the thermostat."""
|
||||
self._client = client
|
||||
self._device = device
|
||||
self._cool_away_temp = cool_away_temp
|
||||
self._heat_away_temp = heat_away_temp
|
||||
self._away = False
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
@property
|
||||
def is_fan_on(self):
|
||||
@@ -236,7 +258,10 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return getattr(self._device, ATTR_SYSTEM_MODE, None)
|
||||
oper = getattr(self._device, ATTR_CURRENT_OPERATION, None)
|
||||
if oper == "off":
|
||||
oper = "idle"
|
||||
return oper
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set target temperature."""
|
||||
@@ -245,29 +270,84 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
return
|
||||
import somecomfort
|
||||
try:
|
||||
if self._device.system_mode == 'cool':
|
||||
self._device.setpoint_cool = temperature
|
||||
else:
|
||||
self._device.setpoint_heat = temperature
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
# Set hold if this is not the case
|
||||
if getattr(self._device, "hold_{}".format(mode)) is False:
|
||||
# Get next period key
|
||||
next_period_key = '{}NextPeriod'.format(mode.capitalize())
|
||||
# Get next period raw value
|
||||
next_period = self._device.raw_ui_data.get(next_period_key)
|
||||
# Get next period time
|
||||
hour, minute = divmod(next_period * 15, 60)
|
||||
# Set hold time
|
||||
setattr(self._device,
|
||||
"hold_{}".format(mode),
|
||||
datetime.time(hour, minute))
|
||||
# Set temperature
|
||||
setattr(self._device,
|
||||
"setpoint_{}".format(mode),
|
||||
temperature)
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Temperature %.1f out of range', temperature)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
import somecomfort
|
||||
data = {
|
||||
ATTR_FAN: (self.is_fan_on and 'running' or 'idle'),
|
||||
ATTR_FANMODE: self._device.fan_mode,
|
||||
ATTR_SYSTEM_MODE: self._device.system_mode,
|
||||
ATTR_FAN_MODE: self._device.fan_mode,
|
||||
ATTR_OPERATION_MODE: self._device.system_mode,
|
||||
}
|
||||
data[ATTR_FAN_LIST] = somecomfort.FAN_MODES
|
||||
data[ATTR_OPERATION_LIST] = somecomfort.SYSTEM_MODES
|
||||
return data
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
pass
|
||||
"""Turn away on.
|
||||
|
||||
Somecomfort does have a proprietary away mode, but it doesn't really
|
||||
work the way it should. For example: If you set a temperature manually
|
||||
it doesn't get overwritten when away mode is switched on.
|
||||
"""
|
||||
self._away = True
|
||||
import somecomfort
|
||||
try:
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Can not get system mode')
|
||||
return
|
||||
try:
|
||||
|
||||
# Set permanent hold
|
||||
setattr(self._device,
|
||||
"hold_{}".format(mode),
|
||||
True)
|
||||
# Set temperature
|
||||
setattr(self._device,
|
||||
"setpoint_{}".format(mode),
|
||||
getattr(self, "_{}_away_temp".format(mode)))
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Temperature %.1f out of range',
|
||||
getattr(self, "_{}_away_temp".format(mode)))
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
pass
|
||||
self._away = False
|
||||
import somecomfort
|
||||
try:
|
||||
# Disabling all hold modes
|
||||
self._device.hold_cool = False
|
||||
self._device.hold_heat = False
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Can not stop hold mode')
|
||||
|
||||
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
|
||||
"""Set the system mode (Cool, Heat, etc)."""
|
||||
@@ -276,4 +356,49 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
self._device.refresh()
|
||||
import somecomfort
|
||||
retries = 3
|
||||
while retries > 0:
|
||||
try:
|
||||
self._device.refresh()
|
||||
break
|
||||
except (somecomfort.client.APIRateLimited, OSError,
|
||||
requests.exceptions.ReadTimeout) as exp:
|
||||
retries -= 1
|
||||
if retries == 0:
|
||||
raise exp
|
||||
if not self._retry():
|
||||
raise exp
|
||||
_LOGGER.error("SomeComfort update failed, Retrying "
|
||||
"- Error: %s", exp)
|
||||
|
||||
def _retry(self):
|
||||
"""Recreate a new somecomfort client.
|
||||
|
||||
When we got an error, the best way to be sure that the next query
|
||||
will succeed, is to recreate a new somecomfort client.
|
||||
"""
|
||||
import somecomfort
|
||||
try:
|
||||
self._client = somecomfort.SomeComfort(self._username,
|
||||
self._password)
|
||||
except somecomfort.AuthError:
|
||||
_LOGGER.error('Failed to login to honeywell account %s',
|
||||
self._username)
|
||||
return False
|
||||
except somecomfort.SomeComfortError as ex:
|
||||
_LOGGER.error('Failed to initialize honeywell client: %s',
|
||||
str(ex))
|
||||
return False
|
||||
|
||||
devices = [device
|
||||
for location in self._client.locations_by_id.values()
|
||||
for device in location.devices_by_id.values()
|
||||
if device.name == self._device.name]
|
||||
|
||||
if len(devices) != 1:
|
||||
_LOGGER.error('Failed to find device %s', self._device.name)
|
||||
return False
|
||||
|
||||
self._device = devices[0]
|
||||
return True
|
||||
|
||||
216
homeassistant/components/climate/maxcube.py
Normal file
216
homeassistant/components/climate/maxcube.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Support for MAX! Thermostats via MAX! Cube.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/maxcube/
|
||||
"""
|
||||
|
||||
import socket
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_MANUAL = "manual"
|
||||
STATE_BOOST = "boost"
|
||||
STATE_VACATION = "vacation"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add thermostats to HASS."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
|
||||
# List of devices
|
||||
devices = []
|
||||
|
||||
for device in cube.devices:
|
||||
# Create device name by concatenating room name + device name
|
||||
name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
# Only add thermostats and wallthermostats
|
||||
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
|
||||
# Add device to HASS
|
||||
devices.append(MaxCubeClimate(hass, name, device.rf_address))
|
||||
|
||||
# Add all devices at once
|
||||
if len(devices) > 0:
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class MaxCubeClimate(ClimateDevice):
|
||||
"""MAX! Cube ClimateDevice."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
"""Initialize MAX! Cube ClimateDevice."""
|
||||
self._name = name
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
|
||||
STATE_VACATION]
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is required."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the ClimateDevice."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return minimum temperature
|
||||
return self.map_temperature_max_hass(device.min_temperature)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return maximum temperature
|
||||
return self.map_temperature_max_hass(device.max_temperature)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return current temperature
|
||||
return self.map_temperature_max_hass(device.actual_temperature)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation (auto, manual, boost, vacation)."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Mode Mapping
|
||||
return self.map_mode_max_hass(device.mode)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return target temperature
|
||||
return self.map_temperature_max_hass(device.target_temperature)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
# Fail is target temperature has not been supplied as argument
|
||||
if kwargs.get(ATTR_TEMPERATURE) is None:
|
||||
return False
|
||||
|
||||
# Determine the new target temperature
|
||||
target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
# Write the target temperature to the MAX! Cube.
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
cube = self._cubehandle.cube
|
||||
|
||||
with self._cubehandle.mutex:
|
||||
try:
|
||||
cube.set_target_temperature(device, target_temperature)
|
||||
except (socket.timeout, socket.error):
|
||||
_LOGGER.error("Setting target temperature failed")
|
||||
return False
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new operation mode."""
|
||||
# Get the device we want to update
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Mode Mapping
|
||||
mode = self.map_mode_hass_max(operation_mode)
|
||||
|
||||
# Write new mode to thermostat
|
||||
if mode is None:
|
||||
return False
|
||||
|
||||
with self._cubehandle.mutex:
|
||||
try:
|
||||
self._cubehandle.cube.set_mode(device, mode)
|
||||
except (socket.timeout, socket.error):
|
||||
_LOGGER.error("Setting operation mode failed")
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Get latest data from MAX! Cube."""
|
||||
# Update the CubeHandle
|
||||
self._cubehandle.update()
|
||||
|
||||
@staticmethod
|
||||
def map_temperature_max_hass(temperature):
|
||||
"""Map Temperature from MAX! to HASS."""
|
||||
if temperature is None:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
return temperature
|
||||
|
||||
@staticmethod
|
||||
def map_mode_hass_max(operation_mode):
|
||||
"""Map HASS Operation Modes to MAX! Operation Modes."""
|
||||
from maxcube.device import \
|
||||
MAX_DEVICE_MODE_AUTOMATIC, \
|
||||
MAX_DEVICE_MODE_MANUAL, \
|
||||
MAX_DEVICE_MODE_VACATION, \
|
||||
MAX_DEVICE_MODE_BOOST
|
||||
|
||||
if operation_mode == STATE_AUTO:
|
||||
mode = MAX_DEVICE_MODE_AUTOMATIC
|
||||
elif operation_mode == STATE_MANUAL:
|
||||
mode = MAX_DEVICE_MODE_MANUAL
|
||||
elif operation_mode == STATE_VACATION:
|
||||
mode = MAX_DEVICE_MODE_VACATION
|
||||
elif operation_mode == STATE_BOOST:
|
||||
mode = MAX_DEVICE_MODE_BOOST
|
||||
else:
|
||||
mode = None
|
||||
|
||||
return mode
|
||||
|
||||
@staticmethod
|
||||
def map_mode_max_hass(mode):
|
||||
"""Map MAX! Operation Modes to HASS Operation Modes."""
|
||||
from maxcube.device import \
|
||||
MAX_DEVICE_MODE_AUTOMATIC, \
|
||||
MAX_DEVICE_MODE_MANUAL, \
|
||||
MAX_DEVICE_MODE_VACATION, \
|
||||
MAX_DEVICE_MODE_BOOST
|
||||
|
||||
if mode == MAX_DEVICE_MODE_AUTOMATIC:
|
||||
operation_mode = STATE_AUTO
|
||||
elif mode == MAX_DEVICE_MODE_MANUAL:
|
||||
operation_mode = STATE_MANUAL
|
||||
elif mode == MAX_DEVICE_MODE_VACATION:
|
||||
operation_mode = STATE_VACATION
|
||||
elif mode == MAX_DEVICE_MODE_BOOST:
|
||||
operation_mode = STATE_BOOST
|
||||
else:
|
||||
operation_mode = None
|
||||
|
||||
return operation_mode
|
||||
292
homeassistant/components/climate/tado.py
Normal file
292
homeassistant/components/climate/tado.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
Tado component to create a climate device for each zone.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.tado/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.components.tado import DATA_TADO
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONST_MODE_SMART_SCHEDULE = 'SMART_SCHEDULE' # Default mytado mode
|
||||
CONST_MODE_OFF = 'OFF' # Switch off heating in a zone
|
||||
|
||||
# When we change the temperature setting, we need an overlay mode
|
||||
# wait until tado changes the mode automatic
|
||||
CONST_OVERLAY_TADO_MODE = 'TADO_MODE'
|
||||
# the user has change the temperature or mode manually
|
||||
CONST_OVERLAY_MANUAL = 'MANUAL'
|
||||
# the temperature will be reset after a timespan
|
||||
CONST_OVERLAY_TIMER = 'TIMER'
|
||||
|
||||
OPERATION_LIST = {
|
||||
CONST_OVERLAY_MANUAL: 'Manual',
|
||||
CONST_OVERLAY_TIMER: 'Timer',
|
||||
CONST_OVERLAY_TADO_MODE: 'Tado mode',
|
||||
CONST_MODE_SMART_SCHEDULE: 'Smart schedule',
|
||||
CONST_MODE_OFF: 'Off',
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tado climate platform."""
|
||||
tado = hass.data[DATA_TADO]
|
||||
|
||||
try:
|
||||
zones = tado.get_zones()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to get zone info from mytado")
|
||||
return False
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
climate_devices.append(create_climate_device(
|
||||
tado, hass, zone, zone['name'], zone['id']))
|
||||
|
||||
if len(climate_devices) > 0:
|
||||
add_devices(climate_devices, True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
"""Create a Tado climate device."""
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
min_temp = float(capabilities['temperatures']['celsius']['min'])
|
||||
max_temp = float(capabilities['temperatures']['celsius']['max'])
|
||||
ac_mode = capabilities['type'] != 'HEATING'
|
||||
|
||||
data_id = 'zone {} {}'.format(name, zone_id)
|
||||
device = TadoClimate(tado,
|
||||
name, zone_id, data_id,
|
||||
hass.config.units.temperature(min_temp, unit),
|
||||
hass.config.units.temperature(max_temp, unit),
|
||||
ac_mode)
|
||||
|
||||
tado.add_sensor(data_id, {
|
||||
'id': zone_id,
|
||||
'zone': zone,
|
||||
'name': name,
|
||||
'climate': device
|
||||
})
|
||||
|
||||
return device
|
||||
|
||||
|
||||
class TadoClimate(ClimateDevice):
|
||||
"""Representation of a tado climate device."""
|
||||
|
||||
def __init__(self, store, zone_name, zone_id, data_id,
|
||||
min_temp, max_temp, ac_mode,
|
||||
tolerance=0.3):
|
||||
"""Initialization of Tado climate device."""
|
||||
self._store = store
|
||||
self._data_id = data_id
|
||||
|
||||
self.zone_name = zone_name
|
||||
self.zone_id = zone_id
|
||||
|
||||
self.ac_mode = ac_mode
|
||||
|
||||
self._active = False
|
||||
self._device_is_active = False
|
||||
|
||||
self._unit = TEMP_CELSIUS
|
||||
self._cur_temp = None
|
||||
self._cur_humidity = None
|
||||
self._is_away = False
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = None
|
||||
self._tolerance = tolerance
|
||||
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self.zone_name
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._cur_humidity
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the sensor temperature."""
|
||||
return self._cur_temp
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current readable operation mode."""
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes (readable)."""
|
||||
return list(OPERATION_LIST.values())
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""The unit of measurement used by the platform."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._is_away
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
self._current_operation = CONST_OVERLAY_TADO_MODE
|
||||
self._overlay_mode = None
|
||||
self._target_temp = temperature
|
||||
self._control_heating()
|
||||
|
||||
def set_operation_mode(self, readable_operation_mode):
|
||||
"""Set new operation mode."""
|
||||
operation_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
for mode, readable in OPERATION_LIST.items():
|
||||
if readable == readable_operation_mode:
|
||||
operation_mode = mode
|
||||
break
|
||||
|
||||
self._current_operation = operation_mode
|
||||
self._overlay_mode = None
|
||||
self._control_heating()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
self._store.update()
|
||||
|
||||
data = self._store.get_data(self._data_id)
|
||||
|
||||
if data is None:
|
||||
_LOGGER.debug("Recieved no data for zone %s", self.zone_name)
|
||||
return
|
||||
|
||||
if 'sensorDataPoints' in data:
|
||||
sensor_data = data['sensorDataPoints']
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
humidity = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
setting = 0
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if 'temperature' in data['setting'] and \
|
||||
data['setting']['temperature'] is not None:
|
||||
setting = float(
|
||||
data['setting']['temperature']['celsius'])
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
self._cur_temp = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
self._cur_humidity = humidity
|
||||
|
||||
if 'tadoMode' in data:
|
||||
mode = data['tadoMode']
|
||||
self._is_away = mode == 'AWAY'
|
||||
|
||||
if 'setting' in data:
|
||||
power = data['setting']['power']
|
||||
if power == 'OFF':
|
||||
self._current_operation = CONST_MODE_OFF
|
||||
self._device_is_active = False
|
||||
else:
|
||||
self._device_is_active = True
|
||||
|
||||
if 'overlay' in data and data['overlay'] is not None:
|
||||
overlay = True
|
||||
termination = data['overlay']['termination']['type']
|
||||
else:
|
||||
overlay = False
|
||||
termination = ""
|
||||
|
||||
# If you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
|
||||
if overlay and self._device_is_active:
|
||||
# There is an overlay the device is on
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
else:
|
||||
# There is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
def _control_heating(self):
|
||||
"""Send new target temperature to mytado."""
|
||||
if not self._active and None not in (
|
||||
self._cur_temp, self._target_temp):
|
||||
self._active = True
|
||||
_LOGGER.info("Obtained current and target temperature. "
|
||||
"Tado thermostat active")
|
||||
|
||||
if not self._active or self._current_operation == self._overlay_mode:
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
||||
_LOGGER.info("Switching mytado.com to SCHEDULE (default) "
|
||||
"for zone %s", self.zone_name)
|
||||
self._store.reset_zone_overlay(self.zone_id)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_OFF:
|
||||
_LOGGER.info("Switching mytado.com to OFF for zone %s",
|
||||
self.zone_name)
|
||||
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
_LOGGER.info("Switching mytado.com to %s mode for zone %s",
|
||||
self._current_operation, self.zone_name)
|
||||
self._store.set_zone_overlay(
|
||||
self.zone_id, self._current_operation, self._target_temp)
|
||||
|
||||
self._overlay_mode = self._current_operation
|
||||
@@ -7,14 +7,14 @@ https://home-assistant.io/components/switch.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE)
|
||||
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@@ -37,6 +37,7 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
@@ -84,11 +85,11 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
return self.vera_device.fan_cycle()
|
||||
|
||||
@property
|
||||
def current_power_mwh(self):
|
||||
"""Current power usage in mWh."""
|
||||
def current_power_w(self):
|
||||
"""Current power usage in W."""
|
||||
power = self.vera_device.power
|
||||
if power:
|
||||
return convert(power, float, 0.0) * 1000
|
||||
return convert(power, float, 0.0)
|
||||
|
||||
def update(self):
|
||||
"""Called by the vera device callback to update state."""
|
||||
|
||||
@@ -19,7 +19,6 @@ DEPENDENCIES = ['wink']
|
||||
STATE_AUX = 'aux'
|
||||
STATE_ECO = 'eco'
|
||||
STATE_FAN = 'fan'
|
||||
SPEED_LOWEST = 'lowest'
|
||||
SPEED_LOW = 'low'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
@@ -400,7 +399,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
op_list.append(STATE_COOL)
|
||||
if 'auto_eco' in modes:
|
||||
op_list.append(STATE_ECO)
|
||||
if 'fan_eco' in modes:
|
||||
if 'fan_only' in modes:
|
||||
op_list.append(STATE_FAN)
|
||||
return op_list
|
||||
|
||||
@@ -439,9 +438,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
def current_fan_mode(self):
|
||||
"""Return the current fan mode."""
|
||||
speed = self.wink.current_fan_speed()
|
||||
if speed <= 0.3 and speed >= 0.0:
|
||||
return SPEED_LOWEST
|
||||
elif speed <= 0.5 and speed > 0.3:
|
||||
if speed <= 0.4 and speed > 0.3:
|
||||
return SPEED_LOW
|
||||
elif speed <= 0.8 and speed > 0.5:
|
||||
return SPEED_MEDIUM
|
||||
@@ -453,14 +450,12 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return [SPEED_LOWEST, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
def set_fan_mode(self, mode):
|
||||
"""Set fan speed."""
|
||||
if mode == SPEED_LOWEST:
|
||||
speed = 0.3
|
||||
elif mode == SPEED_LOW:
|
||||
speed = 0.5
|
||||
if mode == SPEED_LOW:
|
||||
speed = 0.4
|
||||
elif mode == SPEED_MEDIUM:
|
||||
speed = 0.8
|
||||
elif mode == SPEED_HIGH:
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
|
||||
@@ -32,29 +32,18 @@ DEVICE_MAPPINGS = {
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Z-Wave Climate devices."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return
|
||||
def get_device(hass, values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZWaveClimate(value, temp_unit)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return ZWaveClimate(values, temp_unit)
|
||||
|
||||
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
def __init__(self, value, temp_unit):
|
||||
def __init__(self, values, temp_unit):
|
||||
"""Initialize the Z-Wave climate device."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._index = value.index
|
||||
self._node = value.node
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_operation = None
|
||||
@@ -69,10 +58,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16))
|
||||
if (self.node.manufacturer_id.strip() and
|
||||
self.node.product_id.strip()):
|
||||
specific_sensor_key = (
|
||||
int(self.node.manufacturer_id, 16),
|
||||
int(self.node.product_id, 16))
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
|
||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||
@@ -83,86 +73,59 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Operation Mode
|
||||
self._current_operation = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, member='data')
|
||||
operation_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
member='data_items')
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
if self.values.mode:
|
||||
self._current_operation = self.values.mode.data
|
||||
operation_list = self.values.mode.data_items
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s", self._current_operation)
|
||||
|
||||
# Current Temp
|
||||
self._current_temperature = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='data')
|
||||
device_unit = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='units')
|
||||
if device_unit is not None:
|
||||
self._unit = device_unit
|
||||
if self.values.temperature:
|
||||
self._current_temperature = self.values.temperature.data
|
||||
device_unit = self.values.temperature.units
|
||||
if device_unit is not None:
|
||||
self._unit = device_unit
|
||||
|
||||
# Fan Mode
|
||||
self._current_fan_mode = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data')
|
||||
fan_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data_items')
|
||||
if fan_list:
|
||||
self._fan_list = list(fan_list)
|
||||
if self.values.fan_mode:
|
||||
self._current_fan_mode = self.values.fan_mode.data
|
||||
fan_list = self.values.fan_mode.data_items
|
||||
if fan_list:
|
||||
self._fan_list = list(fan_list)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
_LOGGER.debug("self._current_fan_mode=%s",
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
self._current_swing_mode = (
|
||||
self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data'))
|
||||
swing_list = self.get_value(class_id=zwave.const
|
||||
.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data_items')
|
||||
if swing_list:
|
||||
self._swing_list = list(swing_list)
|
||||
if self.values.zxt_120_swing_mode:
|
||||
self._current_swing_mode = self.values.zxt_120_swing_mode.data
|
||||
swing_list = self.values.zxt_120_swing_mode.data_items
|
||||
if swing_list:
|
||||
self._swing_list = list(swing_list)
|
||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
temps = []
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
temps.append((round(float(value.data)), 1))
|
||||
if value.index == self._index:
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
break
|
||||
else:
|
||||
self._target_temperature = round((float(value.data)), 1)
|
||||
if self.values.primary.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
if self._current_temperature is not None:
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
else:
|
||||
self._target_temperature = round(
|
||||
(float(self.values.primary.data)), 1)
|
||||
|
||||
# Operating state
|
||||
self._operating_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
|
||||
member='data')
|
||||
if self.values.operating_state:
|
||||
self._operating_state = self.values.operating_state.data
|
||||
|
||||
# Fan operating state
|
||||
self._fan_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE,
|
||||
member='data')
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling on Z-Wave."""
|
||||
return False
|
||||
if self.values.fan_state:
|
||||
self._fan_state = self.values.fan_state.data
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
@@ -221,36 +184,31 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
else:
|
||||
return
|
||||
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT,
|
||||
index=self._index, data=temperature)
|
||||
self.update_ha_state()
|
||||
self.values.primary.data = temperature
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
index=0, data=bytes(fan, 'utf-8'))
|
||||
if self.values.fan_mode:
|
||||
self.values.fan_mode.data = bytes(fan, 'utf-8')
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
index=0, data=bytes(operation_mode, 'utf-8'))
|
||||
if self.values.mode:
|
||||
self.values.mode.data = bytes(operation_mode, 'utf-8')
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
if self._zxt_120 == 1:
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33, data=bytes(swing_mode, 'utf-8'))
|
||||
if self.values.zxt_120_swing_mode:
|
||||
self.values.zxt_120_swing_mode.data = bytes(
|
||||
swing_mode, 'utf-8')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
data = super().device_state_attributes
|
||||
if self._operating_state:
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state,
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state
|
||||
if self._fan_state:
|
||||
data[ATTR_FAN_STATE] = self._fan_state
|
||||
return data
|
||||
|
||||
@@ -6,7 +6,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.bootstrap import (
|
||||
from homeassistant.setup import (
|
||||
async_prepare_setup_platform, ATTR_COMPONENT)
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
@@ -180,7 +180,7 @@ class Configurator(object):
|
||||
|
||||
# field validation goes here?
|
||||
|
||||
callback(call.data.get(ATTR_FIELDS, {}))
|
||||
self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
|
||||
|
||||
def _generate_unique_id(self):
|
||||
"""Generate a unique configurator ID."""
|
||||
|
||||
@@ -166,7 +166,7 @@ def async_setup(hass, config):
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
cover.async_update_ha_state(True))
|
||||
if hasattr(cover, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
@@ -54,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
yield from async_add_devices([MqttCover(
|
||||
async_add_devices([MqttCover(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
||||
@@ -14,8 +14,8 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.6.zip'
|
||||
'#pymyq==0.0.6']
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
|
||||
'#pymyq==0.0.8']
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): cv.string,
|
||||
|
||||
@@ -6,9 +6,9 @@ https://home-assistant.io/components/cover.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@@ -28,6 +28,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
@@ -46,6 +47,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
self.vera_device.set_level(position)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
@@ -59,11 +61,14 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self.vera_device.open()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self.vera_device.close()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self.vera_device.stop()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.cover import (
|
||||
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.components.zwave import workaround
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
|
||||
@@ -19,42 +20,34 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave covers."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
|
||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and value.index == 0):
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if (value.type != zwave.const.TYPE_BOOL and
|
||||
value.genre != zwave.const.GENRE_USER):
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
else:
|
||||
return
|
||||
def get_device(hass, values, node_config, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
invert_buttons = node_config.get(zwave.CONF_INVERT_OPENCLOSE_BUTTONS)
|
||||
if (values.primary.command_class ==
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and values.primary.index == 0):
|
||||
return ZwaveRollershutter(hass, values, invert_buttons)
|
||||
elif (values.primary.command_class in [
|
||||
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
||||
return ZwaveGarageDoor(values)
|
||||
return None
|
||||
|
||||
|
||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave roller shutter."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, hass, values, invert_buttons):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._node = value.node
|
||||
self._network = hass.data[zwave.ZWAVE_NETWORK]
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
self._current_position = None
|
||||
self._invert_buttons = invert_buttons
|
||||
|
||||
self._workaround = workaround.get_device_mapping(value)
|
||||
self._workaround = workaround.get_device_mapping(values.primary)
|
||||
if self._workaround:
|
||||
_LOGGER.debug("Using workaround %s", self._workaround)
|
||||
self.update_properties()
|
||||
@@ -62,17 +55,16 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Position value
|
||||
self._current_position = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Level'], member='data')
|
||||
self._open_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Open', 'Up'], member='value_id')
|
||||
self._close_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Close', 'Down'], member='value_id')
|
||||
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
|
||||
self._open_id, self._close_id = self._close_id, self._open_id
|
||||
self._current_position = self.values.primary.data
|
||||
|
||||
if self.values.open and self.values.close and \
|
||||
self._open_id is None and self._close_id is None:
|
||||
if self._invert_buttons:
|
||||
self._open_id = self.values.close.value_id
|
||||
self._close_id = self.values.open.value_id
|
||||
else:
|
||||
self._open_id = self.values.open.value_id
|
||||
self._close_id = self.values.close.value_id
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
@@ -99,32 +91,32 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
zwave.NETWORK.manager.pressButton(self._open_id)
|
||||
self._network.manager.pressButton(self._open_id)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
zwave.NETWORK.manager.pressButton(self._close_id)
|
||||
self._network.manager.pressButton(self._close_id)
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
self._node.set_dimmer(self._value.value_id, position)
|
||||
self.node.set_dimmer(self.values.primary.value_id, position)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
zwave.NETWORK.manager.releaseButton(self._open_id)
|
||||
self._network.manager.releaseButton(self._open_id)
|
||||
|
||||
|
||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave garage door device."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, values):
|
||||
"""Initialize the zwave garage door."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
@@ -133,11 +125,11 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the garage door."""
|
||||
self._value.data = False
|
||||
self.values.primary.data = False
|
||||
|
||||
def open_cover(self):
|
||||
"""Open the garage door."""
|
||||
self._value.data = True
|
||||
self.values.primary.data = True
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -4,6 +4,7 @@ Sets up a demo environment that mimics interaction with devices.
|
||||
For more details about this component, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
@@ -34,7 +35,8 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup a demo environment."""
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
@@ -44,7 +46,7 @@ def setup(hass, config):
|
||||
config.setdefault(DOMAIN, {})
|
||||
|
||||
if config[DOMAIN].get('hide_demo_state') != 1:
|
||||
hass.states.set('a.Demo_Mode', 'Enabled')
|
||||
hass.states.async_set('a.Demo_Mode', 'Enabled')
|
||||
|
||||
# Setup sun
|
||||
if not hass.config.latitude:
|
||||
@@ -53,50 +55,71 @@ def setup(hass, config):
|
||||
if not hass.config.longitude:
|
||||
hass.config.longitude = 117.22743
|
||||
|
||||
bootstrap.setup_component(hass, 'sun')
|
||||
tasks = [
|
||||
bootstrap.async_setup_component(hass, 'sun')
|
||||
]
|
||||
|
||||
# Setup demo platforms
|
||||
demo_config = config.copy()
|
||||
for component in COMPONENTS_WITH_DEMO_PLATFORM:
|
||||
demo_config[component] = {CONF_PLATFORM: 'demo'}
|
||||
bootstrap.setup_component(hass, component, demo_config)
|
||||
tasks.append(
|
||||
bootstrap.async_setup_component(hass, component, demo_config))
|
||||
|
||||
# Set up input select
|
||||
tasks.append(bootstrap.async_setup_component(
|
||||
hass, 'input_select',
|
||||
{'input_select':
|
||||
{'living_room_preset': {'options': ['Visitors',
|
||||
'Visitors with kids',
|
||||
'Home Alone']},
|
||||
'who_cooks': {'icon': 'mdi:panda',
|
||||
'initial': 'Anne Therese',
|
||||
'name': 'Cook today',
|
||||
'options': ['Paulus', 'Anne Therese']}}}))
|
||||
# Set up input boolean
|
||||
tasks.append(bootstrap.async_setup_component(
|
||||
hass, 'input_boolean',
|
||||
{'input_boolean': {'notify': {
|
||||
'icon': 'mdi:car',
|
||||
'initial': False,
|
||||
'name': 'Notify Anne Therese is home'}}}))
|
||||
|
||||
# Set up input boolean
|
||||
tasks.append(bootstrap.async_setup_component(
|
||||
hass, 'input_slider',
|
||||
{'input_slider': {
|
||||
'noise_allowance': {'icon': 'mdi:bell-ring',
|
||||
'min': 0,
|
||||
'max': 10,
|
||||
'name': 'Allowed Noise',
|
||||
'unit_of_measurement': 'dB'}}}))
|
||||
|
||||
# Set up weblink
|
||||
tasks.append(bootstrap.async_setup_component(
|
||||
hass, 'weblink',
|
||||
{'weblink': {'entities': [{'name': 'Router',
|
||||
'url': 'http://192.168.1.1'}]}}))
|
||||
|
||||
results = yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
|
||||
if any(not result for result in results):
|
||||
return False
|
||||
|
||||
# Setup example persistent notification
|
||||
persistent_notification.create(
|
||||
persistent_notification.async_create(
|
||||
hass, 'This is an example of a persistent notification.',
|
||||
title='Example Notification')
|
||||
|
||||
# Setup room groups
|
||||
lights = sorted(hass.states.entity_ids('light'))
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
media_players = sorted(hass.states.entity_ids('media_player'))
|
||||
lights = sorted(hass.states.async_entity_ids('light'))
|
||||
switches = sorted(hass.states.async_entity_ids('switch'))
|
||||
media_players = sorted(hass.states.async_entity_ids('media_player'))
|
||||
|
||||
group.Group.create_group(hass, 'living room', [
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'scene.romantic_lights'])
|
||||
group.Group.create_group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance'])
|
||||
group.Group.create_group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
|
||||
group.Group.create_group(hass, 'doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door'])
|
||||
group.Group.create_group(hass, 'automations', [
|
||||
'input_select.who_cooks', 'input_boolean.notify', ])
|
||||
group.Group.create_group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus'])
|
||||
group.Group.create_group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window', 'group.doors',
|
||||
'thermostat.ecobee',
|
||||
], view=True)
|
||||
tasks2 = []
|
||||
|
||||
# Setup scripts
|
||||
bootstrap.setup_component(
|
||||
tasks2.append(bootstrap.async_setup_component(
|
||||
hass, 'script',
|
||||
{'script': {
|
||||
'demo': {
|
||||
@@ -115,10 +138,10 @@ def setup(hass, config):
|
||||
'service': 'light.turn_off',
|
||||
'data': {ATTR_ENTITY_ID: lights[0]}
|
||||
}]
|
||||
}}})
|
||||
}}}))
|
||||
|
||||
# Setup scenes
|
||||
bootstrap.setup_component(
|
||||
tasks2.append(bootstrap.async_setup_component(
|
||||
hass, 'scene',
|
||||
{'scene': [
|
||||
{'name': 'Romantic lights',
|
||||
@@ -132,41 +155,37 @@ def setup(hass, config):
|
||||
switches[0]: True,
|
||||
switches[1]: False,
|
||||
}},
|
||||
]})
|
||||
]}))
|
||||
|
||||
# Set up input select
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_select',
|
||||
{'input_select':
|
||||
{'living_room_preset': {'options': ['Visitors',
|
||||
'Visitors with kids',
|
||||
'Home Alone']},
|
||||
'who_cooks': {'icon': 'mdi:panda',
|
||||
'initial': 'Anne Therese',
|
||||
'name': 'Cook today',
|
||||
'options': ['Paulus', 'Anne Therese']}}})
|
||||
# Set up input boolean
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_boolean',
|
||||
{'input_boolean': {'notify': {'icon': 'mdi:car',
|
||||
'initial': False,
|
||||
'name': 'Notify Anne Therese is home'}}})
|
||||
tasks2.append(group.Group.async_create_group(hass, 'living room', [
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'cover.living_room_window', media_players[1],
|
||||
'scene.romantic_lights']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'kitchen', [
|
||||
lights[2], 'cover.kitchen_window', 'lock.kitchen_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'automations', [
|
||||
'input_select.who_cooks', 'input_boolean.notify', ]))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'cover.kitchen_window',
|
||||
'cover.living_room_window', 'group.doors',
|
||||
'thermostat.ecobee',
|
||||
], view=True))
|
||||
|
||||
# Set up input boolean
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_slider',
|
||||
{'input_slider': {
|
||||
'noise_allowance': {'icon': 'mdi:bell-ring',
|
||||
'min': 0,
|
||||
'max': 10,
|
||||
'name': 'Allowed Noise',
|
||||
'unit_of_measurement': 'dB'}}})
|
||||
results = yield from asyncio.gather(*tasks2, loop=hass.loop)
|
||||
|
||||
if any(not result for result in results):
|
||||
return False
|
||||
|
||||
# Set up weblink
|
||||
bootstrap.setup_component(
|
||||
hass, 'weblink',
|
||||
{'weblink': {'entities': [{'name': 'Router',
|
||||
'url': 'http://192.168.1.1'}]}})
|
||||
# Setup configurator
|
||||
configurator_ids = []
|
||||
|
||||
@@ -184,14 +203,17 @@ def setup(hass, config):
|
||||
else:
|
||||
configurator.request_done(configurator_ids[0])
|
||||
|
||||
request_id = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
description=("Press the button on the bridge to register Philips Hue "
|
||||
"with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
def setup_configurator():
|
||||
"""Setup configurator."""
|
||||
request_id = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
description=("Press the button on the bridge to register Philips "
|
||||
"Hue with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
configurator_ids.append(request_id)
|
||||
|
||||
configurator_ids.append(request_id)
|
||||
hass.async_add_job(setup_configurator)
|
||||
|
||||
return True
|
||||
|
||||
@@ -14,12 +14,11 @@ import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import (
|
||||
async_prepare_setup_platform, async_log_exception)
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
@@ -133,18 +132,6 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
||||
|
||||
# added_to_hass
|
||||
add_tasks = [device.async_added_to_hass() for device in devices
|
||||
if device.track]
|
||||
if add_tasks:
|
||||
yield from asyncio.wait(add_tasks, loop=hass.loop)
|
||||
|
||||
# update tracked devices
|
||||
update_tasks = [device.async_update_ha_state() for device in devices
|
||||
if device.track]
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(p_type, p_config, disc_info=None):
|
||||
"""Setup a device tracker platform."""
|
||||
@@ -227,6 +214,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
|
||||
|
||||
# restore
|
||||
yield from tracker.async_setup_tracked_device()
|
||||
return True
|
||||
|
||||
|
||||
@@ -357,6 +346,27 @@ class DeviceTracker(object):
|
||||
device.stale(now):
|
||||
self.hass.async_add_job(device.async_update_ha_state(True))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_tracked_device(self):
|
||||
"""Setup all not exists tracked devices.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def async_init_single_device(dev):
|
||||
"""Init a single device_tracker entity."""
|
||||
yield from dev.async_added_to_hass()
|
||||
yield from dev.async_update_ha_state()
|
||||
|
||||
tasks = []
|
||||
for device in self.devices.values():
|
||||
if device.track and not device.last_seen:
|
||||
tasks.append(self.hass.async_add_job(
|
||||
async_init_single_device(device)))
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=self.hass.loop)
|
||||
|
||||
|
||||
class Device(Entity):
|
||||
"""Represent a tracked device."""
|
||||
@@ -543,7 +553,6 @@ class Device(Entity):
|
||||
# bytes like 00 get truncates to 0, API needs full bytes
|
||||
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
||||
url = 'http://api.macvendors.com/' + oui
|
||||
resp = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
|
||||
@@ -560,13 +569,9 @@ class Device(Entity):
|
||||
# in the 'known_devices.yaml' file which only happens
|
||||
# the first time the device is seen.
|
||||
return 'unknown'
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError):
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
# same as above
|
||||
return 'unknown'
|
||||
finally:
|
||||
if resp is not None:
|
||||
yield from resp.release()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
|
||||
@@ -4,19 +4,20 @@ Support for the Automatic platform.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.automatic/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, ATTR_ATTRIBUTES)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import datetime as dt_util
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
REQUIREMENTS = ['aioautomatic==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,129 +25,101 @@ CONF_CLIENT_ID = 'client_id'
|
||||
CONF_SECRET = 'secret'
|
||||
CONF_DEVICES = 'devices'
|
||||
|
||||
SCOPE = 'scope:location scope:vehicle:profile scope:user:profile scope:trip'
|
||||
|
||||
ATTR_ACCESS_TOKEN = 'access_token'
|
||||
ATTR_EXPIRES_IN = 'expires_in'
|
||||
ATTR_RESULTS = 'results'
|
||||
ATTR_VEHICLE = 'vehicle'
|
||||
ATTR_ENDED_AT = 'ended_at'
|
||||
ATTR_END_LOCATION = 'end_location'
|
||||
|
||||
URL_AUTHORIZE = 'https://accounts.automatic.com/oauth/access_token/'
|
||||
URL_VEHICLES = 'https://api.automatic.com/vehicle/'
|
||||
URL_TRIPS = 'https://api.automatic.com/trip/'
|
||||
|
||||
_VEHICLE_ID_REGEX = re.compile(
|
||||
(URL_VEHICLES + '(.*)?[/]$').replace('/', r'\/'))
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_SECRET): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string])
|
||||
vol.Optional(CONF_DEVICES, default=None): vol.All(
|
||||
cv.ensure_list, [cv.string])
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Validate the configuration and return an Automatic scanner."""
|
||||
import aioautomatic
|
||||
|
||||
client = aioautomatic.Client(
|
||||
client_id=config[CONF_CLIENT_ID],
|
||||
client_secret=config[CONF_SECRET],
|
||||
client_session=async_get_clientsession(hass),
|
||||
request_kwargs={'timeout': DEFAULT_TIMEOUT})
|
||||
try:
|
||||
AutomaticDeviceScanner(hass, config, see)
|
||||
except requests.HTTPError as err:
|
||||
session = yield from client.create_session_from_password(
|
||||
config[CONF_USERNAME], config[CONF_PASSWORD])
|
||||
data = AutomaticData(hass, session, config[CONF_DEVICES], async_see)
|
||||
except aioautomatic.exceptions.AutomaticError as err:
|
||||
_LOGGER.error(str(err))
|
||||
return False
|
||||
|
||||
yield from data.update()
|
||||
return True
|
||||
|
||||
|
||||
class AutomaticDeviceScanner(object):
|
||||
"""A class representing an Automatic device."""
|
||||
class AutomaticData(object):
|
||||
"""A class representing an Automatic cloud service connection."""
|
||||
|
||||
def __init__(self, hass, config: dict, see) -> None:
|
||||
def __init__(self, hass, session, devices, async_see):
|
||||
"""Initialize the automatic device scanner."""
|
||||
self.hass = hass
|
||||
self._devices = config.get(CONF_DEVICES, None)
|
||||
self._access_token_payload = {
|
||||
'username': config.get(CONF_USERNAME),
|
||||
'password': config.get(CONF_PASSWORD),
|
||||
'client_id': config.get(CONF_CLIENT_ID),
|
||||
'client_secret': config.get(CONF_SECRET),
|
||||
'grant_type': 'password',
|
||||
'scope': SCOPE
|
||||
}
|
||||
self._headers = None
|
||||
self._token_expires = dt_util.now()
|
||||
self.last_results = {}
|
||||
self.last_trips = {}
|
||||
self.see = see
|
||||
self.devices = devices
|
||||
self.session = session
|
||||
self.async_see = async_see
|
||||
|
||||
self._update_info()
|
||||
async_track_time_interval(hass, self.update, timedelta(seconds=30))
|
||||
|
||||
track_utc_time_change(self.hass, self._update_info,
|
||||
second=range(0, 60, 30))
|
||||
|
||||
def _update_headers(self):
|
||||
"""Get the access token from automatic."""
|
||||
if self._headers is None or self._token_expires <= dt_util.now():
|
||||
resp = requests.post(
|
||||
URL_AUTHORIZE,
|
||||
data=self._access_token_payload)
|
||||
|
||||
resp.raise_for_status()
|
||||
|
||||
json = resp.json()
|
||||
|
||||
access_token = json[ATTR_ACCESS_TOKEN]
|
||||
self._token_expires = dt_util.now() + timedelta(
|
||||
seconds=json[ATTR_EXPIRES_IN])
|
||||
self._headers = {
|
||||
'Authorization': 'Bearer {}'.format(access_token)
|
||||
}
|
||||
|
||||
def _update_info(self, now=None) -> None:
|
||||
@asyncio.coroutine
|
||||
def update(self, now=None):
|
||||
"""Update the device info."""
|
||||
import aioautomatic
|
||||
|
||||
_LOGGER.debug('Updating devices %s', now)
|
||||
self._update_headers()
|
||||
|
||||
response = requests.get(URL_VEHICLES, headers=self._headers)
|
||||
try:
|
||||
vehicles = yield from self.session.get_vehicles()
|
||||
except aioautomatic.exceptions.AutomaticError as err:
|
||||
_LOGGER.error(str(err))
|
||||
return False
|
||||
|
||||
response.raise_for_status()
|
||||
for vehicle in vehicles:
|
||||
name = vehicle.display_name
|
||||
if name is None:
|
||||
name = ' '.join(filter(None, (
|
||||
str(vehicle.year), vehicle.make, vehicle.model)))
|
||||
|
||||
self.last_results = [item for item in response.json()[ATTR_RESULTS]
|
||||
if self._devices is None or item[
|
||||
'display_name'] in self._devices]
|
||||
if self.devices is not None and name not in self.devices:
|
||||
continue
|
||||
|
||||
response = requests.get(URL_TRIPS, headers=self._headers)
|
||||
self.hass.async_add_job(self.update_vehicle(vehicle, name))
|
||||
|
||||
if response.status_code == 200:
|
||||
for trip in response.json()[ATTR_RESULTS]:
|
||||
vehicle_id = _VEHICLE_ID_REGEX.match(
|
||||
trip[ATTR_VEHICLE]).group(1)
|
||||
if vehicle_id not in self.last_trips:
|
||||
self.last_trips[vehicle_id] = trip
|
||||
elif self.last_trips[vehicle_id][ATTR_ENDED_AT] < trip[
|
||||
ATTR_ENDED_AT]:
|
||||
self.last_trips[vehicle_id] = trip
|
||||
@asyncio.coroutine
|
||||
def update_vehicle(self, vehicle, name):
|
||||
"""Updated the specified vehicle's data."""
|
||||
import aioautomatic
|
||||
|
||||
for vehicle in self.last_results:
|
||||
dev_id = vehicle.get('id')
|
||||
host_name = vehicle.get('display_name')
|
||||
kwargs = {
|
||||
'dev_id': vehicle.id,
|
||||
'host_name': name,
|
||||
'mac': vehicle.id,
|
||||
ATTR_ATTRIBUTES: {
|
||||
'fuel_level': vehicle.fuel_level_percent,
|
||||
}
|
||||
}
|
||||
|
||||
attrs = {
|
||||
'fuel_level': vehicle.get('fuel_level_percent')
|
||||
}
|
||||
trips = []
|
||||
try:
|
||||
# Get the most recent trip for this vehicle
|
||||
trips = yield from self.session.get_trips(
|
||||
vehicle=vehicle.id, limit=1)
|
||||
except aioautomatic.exceptions.AutomaticError as err:
|
||||
_LOGGER.error(str(err))
|
||||
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
'host_name': host_name,
|
||||
'mac': dev_id,
|
||||
ATTR_ATTRIBUTES: attrs
|
||||
}
|
||||
if trips:
|
||||
end_location = trips[0].end_location
|
||||
kwargs['gps'] = (end_location.lat, end_location.lon)
|
||||
kwargs['gps_accuracy'] = end_location.accuracy_m
|
||||
|
||||
if dev_id in self.last_trips:
|
||||
end_location = self.last_trips[dev_id][ATTR_END_LOCATION]
|
||||
kwargs['gps'] = (end_location['lat'], end_location['lon'])
|
||||
kwargs['gps_accuracy'] = end_location['accuracy_m']
|
||||
|
||||
self.see(**kwargs)
|
||||
yield from self.async_see(**kwargs)
|
||||
|
||||
@@ -110,7 +110,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
_LOGGER.info("Discovered Bluetooth LE device %s", address)
|
||||
see_device(address, devs[address], new_device=True)
|
||||
|
||||
track_point_in_utc_time(hass, update_ble, now + interval)
|
||||
track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval)
|
||||
|
||||
update_ble(dt_util.utcnow())
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
see_device((mac, result))
|
||||
except bluetooth.BluetoothError:
|
||||
_LOGGER.exception('Error looking up bluetooth device!')
|
||||
track_point_in_utc_time(hass, update_bluetooth, now + interval)
|
||||
track_point_in_utc_time(
|
||||
hass, update_bluetooth, dt_util.utcnow() + interval)
|
||||
|
||||
update_bluetooth(dt_util.utcnow())
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.6']
|
||||
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -197,13 +197,22 @@ class Icloud(DeviceScanner):
|
||||
|
||||
def icloud_trusted_device_callback(self, callback_data):
|
||||
"""The trusted device is chosen."""
|
||||
self._trusted_device = int(callback_data.get('0', '0'))
|
||||
self._trusted_device = int(callback_data.get('trusted_device'))
|
||||
self._trusted_device = self.api.trusted_devices[self._trusted_device]
|
||||
|
||||
if not self.api.send_verification_code(self._trusted_device):
|
||||
_LOGGER.error('Failed to send verification code')
|
||||
self._trusted_device = None
|
||||
return
|
||||
|
||||
if self.accountname in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(self.accountname)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
|
||||
# Trigger the next step immediately
|
||||
self.icloud_need_verification_code()
|
||||
|
||||
def icloud_need_trusted_device(self):
|
||||
"""We need a trusted device."""
|
||||
configurator = get_component('configurator')
|
||||
@@ -213,7 +222,10 @@ class Icloud(DeviceScanner):
|
||||
devicesstring = ''
|
||||
devices = self.api.trusted_devices
|
||||
for i, device in enumerate(devices):
|
||||
devicesstring += "{}: {};".format(i, device.get('deviceName'))
|
||||
devicename = device.get(
|
||||
'deviceName',
|
||||
'SMS to %s' % device.get('phoneNumber'))
|
||||
devicesstring += "{}: {};".format(i, devicename)
|
||||
|
||||
_CONFIGURING[self.accountname] = configurator.request_config(
|
||||
self.hass, 'iCloud {}'.format(self.accountname),
|
||||
@@ -223,12 +235,27 @@ class Icloud(DeviceScanner):
|
||||
' the index from this list: ' + devicesstring),
|
||||
entity_picture="/static/images/config_icloud.png",
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': '0'}]
|
||||
fields=[{'id': 'trusted_device', 'name': 'Trusted Device'}]
|
||||
)
|
||||
|
||||
def icloud_verification_callback(self, callback_data):
|
||||
"""The trusted device is chosen."""
|
||||
self._verification_code = callback_data.get('0')
|
||||
from pyicloud.exceptions import PyiCloudException
|
||||
self._verification_code = callback_data.get('code')
|
||||
|
||||
try:
|
||||
if not self.api.validate_verification_code(
|
||||
self._trusted_device, self._verification_code):
|
||||
raise PyiCloudException('Unknown failure')
|
||||
except PyiCloudException as error:
|
||||
# Reset to the inital 2FA state to allow the user to retry
|
||||
_LOGGER.error('Failed to verify verification code: %s', error)
|
||||
self._trusted_device = None
|
||||
self._verification_code = None
|
||||
|
||||
# Trigger the next step immediately
|
||||
self.icloud_need_trusted_device()
|
||||
|
||||
if self.accountname in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(self.accountname)
|
||||
configurator = get_component('configurator')
|
||||
@@ -240,22 +267,17 @@ class Icloud(DeviceScanner):
|
||||
if self.accountname in _CONFIGURING:
|
||||
return
|
||||
|
||||
if self.api.send_verification_code(self._trusted_device):
|
||||
self._verification_code = 'waiting'
|
||||
|
||||
_CONFIGURING[self.accountname] = configurator.request_config(
|
||||
self.hass, 'iCloud {}'.format(self.accountname),
|
||||
self.icloud_verification_callback,
|
||||
description=('Please enter the validation code:'),
|
||||
entity_picture="/static/images/config_icloud.png",
|
||||
submit_caption='Confirm',
|
||||
fields=[{'code': '0'}]
|
||||
fields=[{'id': 'code', 'name': 'code'}]
|
||||
)
|
||||
|
||||
def keep_alive(self, now):
|
||||
"""Keep the api alive."""
|
||||
from pyicloud.exceptions import PyiCloud2FARequiredError
|
||||
|
||||
if self.api is None:
|
||||
self.reset_account_icloud()
|
||||
|
||||
@@ -263,9 +285,8 @@ class Icloud(DeviceScanner):
|
||||
return
|
||||
|
||||
if self.api.requires_2fa:
|
||||
from pyicloud.exceptions import PyiCloudException
|
||||
try:
|
||||
self.api.authenticate()
|
||||
except PyiCloud2FARequiredError:
|
||||
if self._trusted_device is None:
|
||||
self.icloud_need_trusted_device()
|
||||
return
|
||||
@@ -274,12 +295,14 @@ class Icloud(DeviceScanner):
|
||||
self.icloud_need_verification_code()
|
||||
return
|
||||
|
||||
if self._verification_code == 'waiting':
|
||||
return
|
||||
self.api.authenticate()
|
||||
if self.api.requires_2fa:
|
||||
raise Exception('Unknown failure')
|
||||
|
||||
if self.api.validate_verification_code(
|
||||
self._trusted_device, self._verification_code):
|
||||
self._verification_code = None
|
||||
self._trusted_device = None
|
||||
self._verification_code = None
|
||||
except PyiCloudException as error:
|
||||
_LOGGER.error("Error setting up 2fa: %s", error)
|
||||
else:
|
||||
self.api.authenticate()
|
||||
|
||||
@@ -397,13 +420,13 @@ class Icloud(DeviceScanner):
|
||||
try:
|
||||
if devicename is not None:
|
||||
if devicename in self.devices:
|
||||
self.devices[devicename].update_icloud()
|
||||
self.devices[devicename].location()
|
||||
else:
|
||||
_LOGGER.error("devicename %s unknown for account %s",
|
||||
devicename, self._attrs[ATTR_ACCOUNTNAME])
|
||||
else:
|
||||
for device in self.devices:
|
||||
self.devices[device].update_icloud()
|
||||
self.devices[device].location()
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error('No iCloud Devices found!')
|
||||
|
||||
|
||||
@@ -18,23 +18,24 @@ from homeassistant.components.device_tracker import ( # NOQA
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
URL = '/api/locative'
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup an endpoint for the Locative application."""
|
||||
"""Set up an endpoint for the Locative application."""
|
||||
hass.http.register_view(LocativeView(see))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class LocativeView(HomeAssistantView):
|
||||
"""View to handle locative requests."""
|
||||
"""View to handle Locative requests."""
|
||||
|
||||
url = '/api/locative'
|
||||
url = URL
|
||||
name = 'api:locative'
|
||||
|
||||
def __init__(self, see):
|
||||
"""Initialize Locative url endpoints."""
|
||||
"""Initialize Locative URL endpoints."""
|
||||
self.see = see
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -51,7 +52,6 @@ class LocativeView(HomeAssistantView):
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=too-many-return-statements
|
||||
def _handle(self, hass, data):
|
||||
"""Handle locative request."""
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
|
||||
@@ -14,6 +14,7 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
@@ -31,6 +32,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
class InvalidLuciTokenError(HomeAssistantError):
|
||||
"""When an invalid token is detected."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Luci scanner."""
|
||||
scanner = LuciDeviceScanner(config[DOMAIN])
|
||||
@@ -46,8 +53,9 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
|
||||
@@ -55,12 +63,15 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.token = _get_token(host, username, password)
|
||||
self.host = host
|
||||
self.refresh_token()
|
||||
|
||||
self.mac2name = None
|
||||
self.success_init = self.token is not None
|
||||
|
||||
def refresh_token(self):
|
||||
"""Get a new token."""
|
||||
self.token = _get_token(self.host, self.username, self.password)
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
@@ -98,8 +109,15 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
_LOGGER.info('Checking ARP')
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
|
||||
try:
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
except InvalidLuciTokenError:
|
||||
_LOGGER.info('Refreshing token')
|
||||
self.refresh_token()
|
||||
return False
|
||||
|
||||
if result:
|
||||
self.last_results = []
|
||||
for device_entry in result:
|
||||
@@ -116,6 +134,7 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
def _req_json_rpc(url, method, *args, **kwargs):
|
||||
"""Perform one JSON RPC operation."""
|
||||
data = json.dumps({'method': method, 'params': args})
|
||||
|
||||
try:
|
||||
res = requests.post(url, data=data, timeout=5, **kwargs)
|
||||
except requests.exceptions.Timeout:
|
||||
@@ -139,6 +158,10 @@ def _req_json_rpc(url, method, *args, **kwargs):
|
||||
"Failed to authenticate, "
|
||||
"please check your username and password")
|
||||
return
|
||||
elif res.status_code == 403:
|
||||
_LOGGER.error('Luci responded with a 403 Invalid token')
|
||||
raise InvalidLuciTokenError
|
||||
|
||||
else:
|
||||
_LOGGER.error('Invalid response from luci: %s', res)
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ Support for tracking MQTT enabled devices.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_DEVICES
|
||||
from homeassistant.components.mqtt import CONF_QOS
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
@@ -23,19 +25,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Setup the MQTT tracker."""
|
||||
devices = config[CONF_DEVICES]
|
||||
qos = config[CONF_QOS]
|
||||
|
||||
dev_id_lookup = {}
|
||||
|
||||
def device_tracker_message_received(topic, payload, qos):
|
||||
@callback
|
||||
def async_tracker_message_received(topic, payload, qos):
|
||||
"""MQTT message received."""
|
||||
see(dev_id=dev_id_lookup[topic], location_name=payload)
|
||||
hass.async_add_job(
|
||||
async_see(dev_id=dev_id_lookup[topic], location_name=payload))
|
||||
|
||||
for dev_id, topic in devices.items():
|
||||
dev_id_lookup[topic] = dev_id
|
||||
mqtt.subscribe(hass, topic, device_tracker_message_received, qos)
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, topic, async_tracker_message_received, qos)
|
||||
|
||||
return True
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user