Compare commits
645 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0270ae05e9 | ||
|
|
9c0b9b9ad6 | ||
|
|
7882ce1afd | ||
|
|
176a078b3c | ||
|
|
5baed6acfb | ||
|
|
f845893f8f | ||
|
|
9c636ab6fd | ||
|
|
0df229773f | ||
|
|
0b404cc0be | ||
|
|
18829daa65 | ||
|
|
f0a138dd51 | ||
|
|
b28114fb5a | ||
|
|
6d83ebc5e4 | ||
|
|
29bd9b4587 | ||
|
|
5ed22f3ef0 | ||
|
|
a14995ed27 | ||
|
|
0a78b69ee2 | ||
|
|
b7ebf3b1eb | ||
|
|
2493155f2b | ||
|
|
e06ff95107 | ||
|
|
eea7824a7e | ||
|
|
a3c2db70e2 | ||
|
|
a784f48022 | ||
|
|
e926426af9 | ||
|
|
bf21d6b4e1 | ||
|
|
dcf4fc5e9b | ||
|
|
f3376ba276 | ||
|
|
1a327d682d | ||
|
|
9c851790dc | ||
|
|
aadf6a7750 | ||
|
|
a03691455b | ||
|
|
1726c4b45a | ||
|
|
9fa1328111 | ||
|
|
f904d06c9a | ||
|
|
253628da11 | ||
|
|
e773526714 | ||
|
|
6dc49ff123 | ||
|
|
6bb6a6ebe9 | ||
|
|
492ade7b1a | ||
|
|
dc9f990ad2 | ||
|
|
d80c05b6b6 | ||
|
|
180a7ec295 | ||
|
|
431f0fd236 | ||
|
|
3d2830278a | ||
|
|
3ac9aaf025 | ||
|
|
0b7b0e54ba | ||
|
|
640a8b5a7f | ||
|
|
915b9cb3eb | ||
|
|
88734f05c6 | ||
|
|
8e6dd62853 | ||
|
|
9948587401 | ||
|
|
3c2b4f5128 | ||
|
|
efe754636a | ||
|
|
8081fe794e | ||
|
|
9a575eb6d6 | ||
|
|
98c77dc08f | ||
|
|
991e292d7e | ||
|
|
7e37634b54 | ||
|
|
5445aafee7 | ||
|
|
7077103c4f | ||
|
|
fc101fbbcb | ||
|
|
21ffe2ed9b | ||
|
|
19fae75669 | ||
|
|
8568773e7d | ||
|
|
5ff9e59b79 | ||
|
|
689939ab9d | ||
|
|
8daaee702b | ||
|
|
e6ad2e8d91 | ||
|
|
dd0b9f2f36 | ||
|
|
0383da7af1 | ||
|
|
b9b1d95514 | ||
|
|
23472cb44d | ||
|
|
0377338a81 | ||
|
|
a3ca3e878b | ||
|
|
d1107a9cf3 | ||
|
|
231656916c | ||
|
|
26526ca57a | ||
|
|
dfad8aa6dc | ||
|
|
496972a587 | ||
|
|
ef3e7b28a9 | ||
|
|
792154a6a7 | ||
|
|
09262a36c4 | ||
|
|
94acda2a31 | ||
|
|
b8492832a6 | ||
|
|
bb22ad3064 | ||
|
|
e36c6b24ee | ||
|
|
ad0224e9aa | ||
|
|
40d7361828 | ||
|
|
ab377f169d | ||
|
|
434a7d6975 | ||
|
|
992be38b94 | ||
|
|
f50c30bbba | ||
|
|
b1b14f0e83 | ||
|
|
b51ba85a15 | ||
|
|
29dbeeb41e | ||
|
|
8b57fd008f | ||
|
|
51d5268f9f | ||
|
|
6f23869a89 | ||
|
|
483b0045fc | ||
|
|
1856e0110b | ||
|
|
c608740382 | ||
|
|
08e694cac3 | ||
|
|
628eacc83e | ||
|
|
ba72166333 | ||
|
|
74f284d2d7 | ||
|
|
a81a8c2bdf | ||
|
|
3686a5ed56 | ||
|
|
e7ead73fad | ||
|
|
a73c2e57a8 | ||
|
|
c39c10a088 | ||
|
|
72fc77b84d | ||
|
|
f4d6ce08e4 | ||
|
|
e9bd5d54ad | ||
|
|
2871ab6bb0 | ||
|
|
cfa69fef1e | ||
|
|
5faba21b8c | ||
|
|
ca1cf44194 | ||
|
|
125059c5ac | ||
|
|
63ba5044b3 | ||
|
|
a93195610a | ||
|
|
d48f6676ab | ||
|
|
794205ad8d | ||
|
|
0e367ceec6 | ||
|
|
44b9771d8a | ||
|
|
122581da7f | ||
|
|
de7e27c92c | ||
|
|
89ec39f629 | ||
|
|
e0cbb92c05 | ||
|
|
b35c44ce04 | ||
|
|
bbff13afee | ||
|
|
ecfcc1fd41 | ||
|
|
86bbfb00ad | ||
|
|
af7f3bd455 | ||
|
|
06a68d0c62 | ||
|
|
99b27b1ec6 | ||
|
|
1a64f14bea | ||
|
|
52a3aa1ca5 | ||
|
|
a94e8f48e0 | ||
|
|
822a263622 | ||
|
|
caa7e770be | ||
|
|
48fbec0a49 | ||
|
|
b5fb382c1c | ||
|
|
d5e652d244 | ||
|
|
548d154cd8 | ||
|
|
55624bcff9 | ||
|
|
3c51d2df0f | ||
|
|
ce3c89db6e | ||
|
|
bf3c0472bb | ||
|
|
6a3c5b093b | ||
|
|
bce4be88dc | ||
|
|
ec8802ec44 | ||
|
|
6e5e97554b | ||
|
|
79783e01d7 | ||
|
|
26983aa646 | ||
|
|
a0f72e3569 | ||
|
|
1620680127 | ||
|
|
4f89230251 | ||
|
|
cdb6f3717d | ||
|
|
ae97218582 | ||
|
|
8c728d1b4e | ||
|
|
fed2c33b54 | ||
|
|
b4990d61f9 | ||
|
|
0eac187d97 | ||
|
|
de6f49c06f | ||
|
|
f1632496f0 | ||
|
|
9c76b30e24 | ||
|
|
78c298e563 | ||
|
|
14707630ae | ||
|
|
8ee4503d7c | ||
|
|
4195254280 | ||
|
|
2484ee53b8 | ||
|
|
4cf618334c | ||
|
|
d4f78e8552 | ||
|
|
34ca1dac7d | ||
|
|
d808d90d26 | ||
|
|
487f3b2951 | ||
|
|
d202929de5 | ||
|
|
6a189eb18d | ||
|
|
67dada226a | ||
|
|
57c2dea02d | ||
|
|
3122c0279f | ||
|
|
843e997292 | ||
|
|
a3ff001eec | ||
|
|
8389a0abe3 | ||
|
|
c21a956895 | ||
|
|
e5c42a676d | ||
|
|
a513e1cc35 | ||
|
|
a0d71c9cb2 | ||
|
|
3441170827 | ||
|
|
c56fa7cfed | ||
|
|
2ea2a62d45 | ||
|
|
a764683f3a | ||
|
|
19cb1a954f | ||
|
|
7a1e2de49f | ||
|
|
08226a4864 | ||
|
|
d570d38d5c | ||
|
|
ae5dfbdf55 | ||
|
|
53f9809567 | ||
|
|
aed9ab0271 | ||
|
|
59029f2830 | ||
|
|
46216c3bda | ||
|
|
aa079625d4 | ||
|
|
dee9244566 | ||
|
|
a6e95db618 | ||
|
|
8f04e03f73 | ||
|
|
d64dae8fcf | ||
|
|
3dd869f0c2 | ||
|
|
e34bfb7381 | ||
|
|
7c431911d1 | ||
|
|
5001c9729f | ||
|
|
32f228f984 | ||
|
|
541fffc7fa | ||
|
|
389c13c891 | ||
|
|
027266ed8b | ||
|
|
ddcad275f7 | ||
|
|
64d5a328f3 | ||
|
|
1b447fb56f | ||
|
|
9bed64e9c0 | ||
|
|
1da94928c6 | ||
|
|
a8f34eb728 | ||
|
|
1002a1b7c9 | ||
|
|
f261aac9cb | ||
|
|
cfbc749000 | ||
|
|
98550b5465 | ||
|
|
034f1b9499 | ||
|
|
c79cd905fe | ||
|
|
294883a174 | ||
|
|
f94319e7cb | ||
|
|
38c50c830f | ||
|
|
925a623445 | ||
|
|
fd5aad1ee7 | ||
|
|
22b4aebeb3 | ||
|
|
89639822f1 | ||
|
|
35a57e1385 | ||
|
|
8c44ecc4ba | ||
|
|
dc0f16c9dd | ||
|
|
16c71ab207 | ||
|
|
06d70544bc | ||
|
|
1877906fdf | ||
|
|
95d033f1af | ||
|
|
7cff107c17 | ||
|
|
89972ed940 | ||
|
|
6694f29918 | ||
|
|
c1798dbe1f | ||
|
|
3246b58437 | ||
|
|
63356fb5eb | ||
|
|
ef64e11b50 | ||
|
|
e38b7d97d2 | ||
|
|
8984a6b161 | ||
|
|
49b595e32e | ||
|
|
a60a342864 | ||
|
|
88b3aa54a8 | ||
|
|
a0c1c918b8 | ||
|
|
675283c23e | ||
|
|
c023d1d656 | ||
|
|
ce4891fe8e | ||
|
|
82d98f5b89 | ||
|
|
2900855061 | ||
|
|
e31d4863c7 | ||
|
|
af736a3e71 | ||
|
|
16feb1c55e | ||
|
|
497bc6ac0d | ||
|
|
cae8f8a006 | ||
|
|
82e992c63c | ||
|
|
3dcafafc6a | ||
|
|
ebcda4076e | ||
|
|
011f82f9e3 | ||
|
|
8ed2c8e6a4 | ||
|
|
b9cadbecaa | ||
|
|
e1db639317 | ||
|
|
beeae17cab | ||
|
|
8fcfb9136c | ||
|
|
62c11dde17 | ||
|
|
e58615b2a5 | ||
|
|
bef2f87ddc | ||
|
|
45a8b74d7f | ||
|
|
09a4336bc5 | ||
|
|
6d60287455 | ||
|
|
6cb91e66c8 | ||
|
|
2189516966 | ||
|
|
1738db9ccc | ||
|
|
e0dd5a8558 | ||
|
|
f4f2da5dc7 | ||
|
|
085d026ab6 | ||
|
|
3b14189021 | ||
|
|
6b9e1f3263 | ||
|
|
bde2f0d5a0 | ||
|
|
50ea3c7744 | ||
|
|
bde9e4e9c0 | ||
|
|
609458052c | ||
|
|
344fb9c8b4 | ||
|
|
03ef74b4ab | ||
|
|
ab63fbff3f | ||
|
|
2ab2f68318 | ||
|
|
5d6c13c12c | ||
|
|
ff5c3c9f98 | ||
|
|
31b8e49ad2 | ||
|
|
978ebb9c59 | ||
|
|
85e3dfe6a6 | ||
|
|
cf5aeebba6 | ||
|
|
3e3d9c881e | ||
|
|
216a756590 | ||
|
|
db23320659 | ||
|
|
c634cbf866 | ||
|
|
ceb332bc31 | ||
|
|
86e3fdee1c | ||
|
|
0f4acb59fe | ||
|
|
c5b2df01d9 | ||
|
|
83a72ab4dc | ||
|
|
2cdef7fb2f | ||
|
|
659d67f362 | ||
|
|
ffccca1f60 | ||
|
|
ef74bd9892 | ||
|
|
3447fdc76f | ||
|
|
a2e45b8fdd | ||
|
|
a65f196d19 | ||
|
|
a74cdc7b0d | ||
|
|
449be29022 | ||
|
|
ba8e417390 | ||
|
|
cad995a5f4 | ||
|
|
06efee7ecf | ||
|
|
bacc14d845 | ||
|
|
6f8a733434 | ||
|
|
906e64fdb5 | ||
|
|
8e406a70f6 | ||
|
|
8d9f4a1754 | ||
|
|
0a53b863cd | ||
|
|
80feb322f9 | ||
|
|
2b514139eb | ||
|
|
2b8dfb2a0e | ||
|
|
6477122b23 | ||
|
|
1e9db41028 | ||
|
|
21d3be4027 | ||
|
|
48b3c98646 | ||
|
|
15803d1773 | ||
|
|
3870d2e0cd | ||
|
|
fe0164b137 | ||
|
|
6bc504bfcc | ||
|
|
c44eefacb4 | ||
|
|
952b1a3e0c | ||
|
|
a57cd58675 | ||
|
|
d67f79e2eb | ||
|
|
d326d187d1 | ||
|
|
d0b1619946 | ||
|
|
21be4c1828 | ||
|
|
d1f4901d53 | ||
|
|
7582eb9f63 | ||
|
|
419ff18afb | ||
|
|
8dd7ebb08e | ||
|
|
5cce02ab62 | ||
|
|
6a816116ab | ||
|
|
bb0f484caf | ||
|
|
3c5c018e3e | ||
|
|
78e7e17484 | ||
|
|
31d2a5d2d1 | ||
|
|
baa9bdf6fc | ||
|
|
00179763ef | ||
|
|
7a73dc7d6a | ||
|
|
d0b9b588a9 | ||
|
|
592c599488 | ||
|
|
6714392e9c | ||
|
|
dc75b28b90 | ||
|
|
d2509ce9e3 | ||
|
|
3afc566be1 | ||
|
|
fb3e388f04 | ||
|
|
254b1c46ac | ||
|
|
d13cc227cc | ||
|
|
446f998759 | ||
|
|
206e7d7a67 | ||
|
|
c3b25f2cd5 | ||
|
|
f3199e7dae | ||
|
|
4ecd724578 | ||
|
|
e4d3b25f1e | ||
|
|
7e7f7b64e5 | ||
|
|
e0e9d3c57b | ||
|
|
a687bdb388 | ||
|
|
199fbc7a15 | ||
|
|
57754cd2ff | ||
|
|
21381a95d4 | ||
|
|
be72b04855 | ||
|
|
86ccf26a1a | ||
|
|
87c138c559 | ||
|
|
b3acd7d21d | ||
|
|
a19f7bff28 | ||
|
|
30b7c6b694 | ||
|
|
43faeff42a | ||
|
|
5ca26fc13f | ||
|
|
04748e3ad1 | ||
|
|
7b02dc434a | ||
|
|
1c1d18053b | ||
|
|
2ac752d67a | ||
|
|
a1ef1c996c | ||
|
|
cbb897b2cf | ||
|
|
e4b67c9574 | ||
|
|
7a8c5a0709 | ||
|
|
aadd730ddd | ||
|
|
68df3deee0 | ||
|
|
c616115419 | ||
|
|
dfe1b8d934 | ||
|
|
ec8dc25c9c | ||
|
|
67a04c2a0e | ||
|
|
600a3e3965 | ||
|
|
3349bdc2bd | ||
|
|
12e26d25a5 | ||
|
|
aa3d0e1047 | ||
|
|
d0ee8abcb8 | ||
|
|
94b47d8bc3 | ||
|
|
7b942243ab | ||
|
|
a70f922a71 | ||
|
|
9ce9b8debb | ||
|
|
d7b006600e | ||
|
|
a564fe8286 | ||
|
|
7fc9fa4b0c | ||
|
|
d87e969671 | ||
|
|
278514b994 | ||
|
|
38b0336694 | ||
|
|
caa096ebd5 | ||
|
|
ba417a730b | ||
|
|
6fa095f4a7 | ||
|
|
5efa076080 | ||
|
|
cbc0833360 | ||
|
|
2e62053629 | ||
|
|
4f09279524 | ||
|
|
57dfce1583 | ||
|
|
33bafb8451 | ||
|
|
f59e242c63 | ||
|
|
cb6f50b7ff | ||
|
|
44177a7fde | ||
|
|
8c505e625b | ||
|
|
314fa42298 | ||
|
|
a80a74b586 | ||
|
|
2508e9f9ff | ||
|
|
71157dbec9 | ||
|
|
1f7792678b | ||
|
|
40840044ca | ||
|
|
2882f05f2c | ||
|
|
b646accf87 | ||
|
|
e7ea6ecf5a | ||
|
|
29343ad651 | ||
|
|
28d86207e1 | ||
|
|
6a01227635 | ||
|
|
b6fb21edaf | ||
|
|
a65a122464 | ||
|
|
5c601f1d5f | ||
|
|
7b8b78ec0e | ||
|
|
38030fcfca | ||
|
|
39913075f4 | ||
|
|
2036c44364 | ||
|
|
3fcc07af04 | ||
|
|
65750f667b | ||
|
|
f07ba1e9a6 | ||
|
|
6e5e0e7acc | ||
|
|
9d7c9d1262 | ||
|
|
42c5475284 | ||
|
|
8e839be938 | ||
|
|
ab48010d14 | ||
|
|
1381984b77 | ||
|
|
6dcf3682df | ||
|
|
65d1f7af50 | ||
|
|
16f4695a13 | ||
|
|
c7ee74a573 | ||
|
|
8e2c1ff4aa | ||
|
|
e437151881 | ||
|
|
81ca175906 | ||
|
|
ebe4c39020 | ||
|
|
952afeb717 | ||
|
|
40be883c0e | ||
|
|
5c87883c86 | ||
|
|
b2b1804f5e | ||
|
|
f5fc4cd97f | ||
|
|
bc78997bbd | ||
|
|
35dd3b8d0d | ||
|
|
491c06f53b | ||
|
|
31c1b7f6ad | ||
|
|
da5b50848a | ||
|
|
586f69ac95 | ||
|
|
3723c3a7e8 | ||
|
|
145c98c40c | ||
|
|
30f74bb3ca | ||
|
|
c9756c40e2 | ||
|
|
b60806583c | ||
|
|
868c08e34b | ||
|
|
71eb09ee5e | ||
|
|
809e613148 | ||
|
|
0dbc023f5b | ||
|
|
b6d75e6c5a | ||
|
|
c78e6c088e | ||
|
|
02f342b670 | ||
|
|
213a738240 | ||
|
|
e4fe8336cc | ||
|
|
068e62623d | ||
|
|
30f5727b40 | ||
|
|
815a6999b1 | ||
|
|
c229d9e90f | ||
|
|
abc353c083 | ||
|
|
38639d26ea | ||
|
|
1c637558bf | ||
|
|
5223d20668 | ||
|
|
ce829d194c | ||
|
|
4a5ad24ae0 | ||
|
|
33cb1b3be6 | ||
|
|
0525af920c | ||
|
|
831799a7af | ||
|
|
8e5da5776d | ||
|
|
be9730cc6c | ||
|
|
e44c2a4016 | ||
|
|
29ffa5c282 | ||
|
|
d7b0929a32 | ||
|
|
31489a56db | ||
|
|
3e09a7360e | ||
|
|
0cdd752d6c | ||
|
|
027c0b3168 | ||
|
|
271546d101 | ||
|
|
d1ed17e7db | ||
|
|
fb2fb5ea73 | ||
|
|
202a8dba8e | ||
|
|
042a482ef1 | ||
|
|
fff413e04e | ||
|
|
e29459a1ae | ||
|
|
49de55e75b | ||
|
|
ee4b1e2b78 | ||
|
|
ed44d28fc0 | ||
|
|
d5f9c1bc01 | ||
|
|
f69c900977 | ||
|
|
9a7ea72fa0 | ||
|
|
fd4a9cf7c5 | ||
|
|
0fe375049a | ||
|
|
69f2f0f34a | ||
|
|
076fdc3f8b | ||
|
|
8887c2a8af | ||
|
|
f4594027fd | ||
|
|
59a0005e5c | ||
|
|
9157f722a4 | ||
|
|
7f2a1c61da | ||
|
|
0eb9516ea7 | ||
|
|
3bb3a70347 | ||
|
|
0262269b00 | ||
|
|
780d62ac5c | ||
|
|
5fca9e170e | ||
|
|
ca7415e935 | ||
|
|
26d3c3b0d6 | ||
|
|
81f8764bb8 | ||
|
|
24d2eaa6ca | ||
|
|
f868df1035 | ||
|
|
d0988422d4 | ||
|
|
f522d95328 | ||
|
|
c856c67790 | ||
|
|
6c5efd5b7e | ||
|
|
c3b6086d80 | ||
|
|
0d93369154 | ||
|
|
4e064f91fd | ||
|
|
f9e53ca22f | ||
|
|
f8bdc835f8 | ||
|
|
1f602be80a | ||
|
|
fe4d971427 | ||
|
|
e5efc2e430 | ||
|
|
537a2a6ef6 | ||
|
|
11cc065845 | ||
|
|
3ac31b2c1b | ||
|
|
a91f937245 | ||
|
|
fed2584d8a | ||
|
|
eaa8e5f29d | ||
|
|
65fbba0e79 | ||
|
|
19522b1f39 | ||
|
|
afe84c2a8b | ||
|
|
952436aa0b | ||
|
|
8a577c8e0d | ||
|
|
bf940bd1f3 | ||
|
|
03e8627b12 | ||
|
|
e886303f08 | ||
|
|
4b0df51b40 | ||
|
|
8494ac7cef | ||
|
|
5076ebe43c | ||
|
|
05b2559df8 | ||
|
|
70b74da3eb | ||
|
|
9e0b107991 | ||
|
|
92d05ccb5c | ||
|
|
bfdb51a558 | ||
|
|
e10b00f341 | ||
|
|
cd87c40bbf | ||
|
|
d02bc3deaa | ||
|
|
1798df7686 | ||
|
|
d505398917 | ||
|
|
70d6ce5b79 | ||
|
|
71452c11c1 | ||
|
|
e30f2bf912 | ||
|
|
262d95b7b1 | ||
|
|
ca3da0e53e | ||
|
|
49882255c4 | ||
|
|
3db31cb951 | ||
|
|
415cfc2537 | ||
|
|
88bb136813 | ||
|
|
4cecc626f4 | ||
|
|
644d5de890 | ||
|
|
148b8c5055 | ||
|
|
09161ae615 | ||
|
|
c1f96aabb0 | ||
|
|
343625d539 | ||
|
|
2e10b4bf67 | ||
|
|
dc8e55fb8b | ||
|
|
d86a5a1e91 | ||
|
|
1327051277 | ||
|
|
712c51e283 | ||
|
|
c96f73d1be | ||
|
|
b3afb386b7 | ||
|
|
2544635921 | ||
|
|
4e5b5f2204 | ||
|
|
98de7c9287 | ||
|
|
80e60efd8f | ||
|
|
b3e9e1dfcd | ||
|
|
40bc49aaae | ||
|
|
3c364fa7e9 | ||
|
|
05946ae5a2 | ||
|
|
ceb0ec5fa4 | ||
|
|
a28196df9a | ||
|
|
c7cc045acd | ||
|
|
225a672a92 | ||
|
|
4d5eb0e3fc | ||
|
|
a68ab07e72 | ||
|
|
ec4fe7e6e6 | ||
|
|
0b4b46d80b | ||
|
|
3bbdd9fedd | ||
|
|
2ed135439a | ||
|
|
1750b22e59 | ||
|
|
9c5e7a9584 | ||
|
|
9b03848a2e | ||
|
|
548d415f94 | ||
|
|
18be276b08 | ||
|
|
9aa9e57890 | ||
|
|
8fe2654862 | ||
|
|
794ff20987 | ||
|
|
fe794d7fd8 | ||
|
|
585fbb1c02 | ||
|
|
e4b697b1ed | ||
|
|
de5533e3c2 | ||
|
|
5aa0158761 | ||
|
|
4d7555957c | ||
|
|
aa34fe15b2 | ||
|
|
1096232e17 | ||
|
|
54ecab7590 | ||
|
|
15e329a588 | ||
|
|
768c98d359 | ||
|
|
6490378de3 | ||
|
|
d0320a9099 | ||
|
|
9116eb166b |
56
.coveragerc
@@ -3,6 +3,8 @@ source = homeassistant
|
||||
|
||||
omit =
|
||||
homeassistant/__main__.py
|
||||
homeassistant/scripts/*.py
|
||||
homeassistant/helpers/typing.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/apcupsd.py
|
||||
@@ -20,6 +22,9 @@ omit =
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
@@ -75,12 +80,31 @@ omit =
|
||||
homeassistant/components/zwave.py
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/switch/knx.py
|
||||
homeassistant/components/binary_sensor/knx.py
|
||||
homeassistant/components/thermostat/knx.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/generic.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
@@ -89,6 +113,7 @@ omit =
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
@@ -103,27 +128,41 @@ omit =
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/garage_door/rpi_gpio.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/directv.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pandora.py
|
||||
homeassistant/components/media_player/pioneer.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/roku.py
|
||||
homeassistant/components/media_player/russound_rnet.py
|
||||
homeassistant/components/media_player/samsungtv.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
@@ -134,8 +173,8 @@ omit =
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/googlevoice.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
@@ -156,25 +195,35 @@ omit =
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/netatmo.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/ohmconnect.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
@@ -184,16 +233,19 @@ omit =
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/thermostat/eq3btsmart.py
|
||||
|
||||
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.tox
|
||||
.git
|
||||
4
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,4 +1,6 @@
|
||||
Make sure you run the latest version before reporting an issue. Feature requests should go in the forum: https://community.home-assistant.io/c/feature-requests
|
||||
Make sure you are running the latest version of Home Assistant before reporting an issue.
|
||||
|
||||
You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum:
|
||||
|
||||
**Home Assistant release (`hass --version`):**
|
||||
|
||||
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,7 +1,7 @@
|
||||
**Description:**
|
||||
|
||||
|
||||
**Related issue (if applicable):** #
|
||||
**Related issue (if applicable):** fixes #
|
||||
|
||||
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||
|
||||
If code communicates with devices:
|
||||
If code communicates with devices, web services, or a:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
@@ -26,8 +26,5 @@ If the code does not interact with devices:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
[fork]: http://stackoverflow.com/a/7244456
|
||||
[squash]: https://github.com/ginatrapani/todo.txt-android/wiki/Squash-All-Commits-Related-to-a-Single-Issue-into-a-Single-Commit
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L16
|
||||
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L51
|
||||
|
||||
|
||||
18
.gitignore
vendored
@@ -7,9 +7,12 @@ 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/config/deps
|
||||
tests/config/home-assistant.log
|
||||
tests/testing_config/deps
|
||||
tests/testing_config/home-assistant.log
|
||||
|
||||
# Hide sublime text stuff
|
||||
*.sublime-project
|
||||
@@ -51,7 +54,8 @@ develop-eggs
|
||||
lib
|
||||
lib64
|
||||
|
||||
# Installer logs
|
||||
# Logs
|
||||
*.log
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
@@ -85,3 +89,11 @@ venv
|
||||
*.swo
|
||||
|
||||
ctags.tmp
|
||||
|
||||
# vagrant stuff
|
||||
virtualization/vagrant/setup_done
|
||||
virtualization/vagrant/.vagrant
|
||||
virtualization/vagrant/config
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
@@ -8,8 +8,13 @@ matrix:
|
||||
env: TOXENV=requirements
|
||||
- python: "3.5"
|
||||
env: TOXENV=lint
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
allow_failures:
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
|
||||
@@ -9,79 +9,5 @@ The process is straight-forward.
|
||||
- Ensure tests work.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
||||
|
||||
Still interested? Then you should read the next sections and get more details.
|
||||
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
|
||||
|
||||
## Adding support for a new device
|
||||
|
||||
For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/).
|
||||
|
||||
After you finish adding support for your device:
|
||||
|
||||
- Check that all dependencies are included via the `REQUIREMENTS` variable in your platform/component and only imported inside functions that use them.
|
||||
- Add any new dependencies to `requirements_all.txt` if needed. Use `script/gen_requirements_all.py`.
|
||||
- Update the `.coveragerc` file to exclude your platform if there are no tests available or your new code uses a 3rd party library for communication with the device/service/sensor.
|
||||
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). It's OK to just add a docstring with configuration details (sample entry for `configuration.yaml` file and alike) to the file header as a start. Visit the [website documentation](https://home-assistant.io/developers/website/) for further information on contributing to [home-assistant.io](https://github.com/home-assistant/home-assistant.io).
|
||||
- Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `tox` or `script/lint`.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
||||
- Check for comments and suggestions on your Pull Request and keep an eye on the [CI output](https://travis-ci.org/home-assistant/home-assistant/).
|
||||
|
||||
If you add a platform for an existing component, there is usually no need for updating the frontend. Only if you've added a new component that should show up in the frontend, there are more steps needed:
|
||||
|
||||
- Update the file [`home-assistant-icons.html`](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html) with an icon for your domain ([pick one from this list](https://www.polymer-project.org/1.0/components/core-elements/demo.html#core-icon)).
|
||||
- Update the demo component with two states that it provides.
|
||||
- Add your component to `home-assistant.conf.example`.
|
||||
|
||||
Since you've updated `home-assistant-icons.html`, you've made changes to the frontend:
|
||||
|
||||
- Run `script/build_frontend`. This will build a new version of the frontend. Make sure you add the changed files `frontend.py` and `frontend.html` to the commit.
|
||||
|
||||
### Setting states
|
||||
|
||||
It is the responsibility of the component to maintain the states of the devices in your domain. Each device should be a single state and, if possible, a group should be provided that tracks the combined state of the devices.
|
||||
|
||||
A state can have several attributes that will help the frontend in displaying your state:
|
||||
|
||||
- `friendly_name`: this name will be used as the name of the device
|
||||
- `entity_picture`: this picture will be shown instead of the domain icon
|
||||
- `unit_of_measurement`: this will be appended to the state in the interface
|
||||
- `hidden`: This is a suggestion to the frontend on if the state should be hidden
|
||||
|
||||
These attributes are defined in [homeassistant.components](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/__init__.py#L25).
|
||||
|
||||
### Proper Visibility Handling
|
||||
|
||||
Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/helpers/entity.py) class. If this is done, visibility will be handled for you.
|
||||
You can set a suggestion for your entity's visibility by setting the hidden property by doing something similar to the following.
|
||||
|
||||
```python
|
||||
self.hidden = True
|
||||
```
|
||||
|
||||
This will SUGGEST that the active frontend hides the entity. This requires that the active frontend support hidden cards (the default frontend does) and that the value of hidden be included in your attributes dictionary (see above). The Entity abstract class will take care of this for you.
|
||||
|
||||
Remember: The suggestion set by your component's code will always be overwritten by user settings in the configuration.yaml file. This is why you may set hidden to be False, but the property may remain True (or vice-versa).
|
||||
|
||||
### Working on the frontend
|
||||
|
||||
The frontend is composed of [Polymer](https://www.polymer-project.org) web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the *http-component* in your config.
|
||||
|
||||
When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works.
|
||||
|
||||
## Testing your code
|
||||
|
||||
To test your code before submission, used the `tox` tool.
|
||||
|
||||
```bash
|
||||
> pip install -U tox
|
||||
> tox
|
||||
```
|
||||
|
||||
This will run unit tests against python 3.4 and 3.5 (if both are available locally), as well as run a set of tests which validate `pep8` and `pylint` style of the code.
|
||||
|
||||
You can optionally run tests on only one tox target using the `-e` option to select an environment.
|
||||
|
||||
For instance `tox -e lint` will run the linters only, `tox -e py34` will run unit tests only on python 3.4.
|
||||
|
||||
### Notes on PyLint and PEP8 validation
|
||||
|
||||
In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change.
|
||||
|
||||
12
Dockerfile
@@ -19,15 +19,9 @@ RUN script/build_python_openzwave && \
|
||||
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
|
||||
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt
|
||||
|
||||
RUN wget http://www.openssl.org/source/openssl-1.0.2h.tar.gz && \
|
||||
tar -xvzf openssl-1.0.2h.tar.gz && \
|
||||
cd openssl-1.0.2h && \
|
||||
./config --prefix=/usr/ && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf openssl-1.0.2h*
|
||||
# certifi breaks Debian based installs
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
|
||||
pip3 install mysqlclient psycopg2
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
14
README.rst
@@ -18,7 +18,7 @@ tutorials and documentation.
|
||||
|
||||
|screenshot-states|
|
||||
|
||||
Examples of devices it can interface it:
|
||||
Examples of devices Home Assistant can interface with:
|
||||
|
||||
- Monitoring connected devices to a wireless router:
|
||||
`OpenWrt <https://openwrt.org/>`__,
|
||||
@@ -61,13 +61,13 @@ Examples of devices it can interface it:
|
||||
- `See full list of supported
|
||||
devices <https://home-assistant.io/components/>`__
|
||||
|
||||
Built home automation on top of your devices:
|
||||
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 sun set
|
||||
- Turn on lights slowly during sun set to compensate for less light
|
||||
- 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/api/>`__
|
||||
- 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
|
||||
@@ -75,10 +75,10 @@ Built home automation on top of your devices:
|
||||
(NMA) <http://www.notifymyandroid.com/>`__,
|
||||
`PushBullet <https://www.pushbullet.com/>`__,
|
||||
`PushOver <https://pushover.net/>`__, `Slack <https://slack.com/>`__,
|
||||
`Telegram <https://telegram.org/>`__, and `Jabber
|
||||
`Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber
|
||||
(XMPP) <http://xmpp.org>`__
|
||||
|
||||
The system is built modular so support for other devices or actions can
|
||||
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
|
||||
|
||||
@@ -7,8 +7,11 @@ homeassistant:
|
||||
latitude: 32.87336
|
||||
longitude: 117.22743
|
||||
|
||||
# C for Celsius, F for Fahrenheit
|
||||
temperature_unit: C
|
||||
# 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
|
||||
@@ -22,6 +25,9 @@ http:
|
||||
# Set to 1 to enable development mode
|
||||
# development: 1
|
||||
|
||||
# Enable the frontend
|
||||
frontend:
|
||||
|
||||
light:
|
||||
# platform: hue
|
||||
|
||||
@@ -30,17 +36,12 @@ wink:
|
||||
access_token: 'YOUR_TOKEN'
|
||||
|
||||
device_tracker:
|
||||
# The following types are available: ddwrt, netgear, tomato, luci,
|
||||
# and nmap_tracker
|
||||
# The following tracker are available:
|
||||
# https://home-assistant.io/components/#presence-detection
|
||||
platform: netgear
|
||||
host: 192.168.1.1
|
||||
username: admin
|
||||
password: PASSWORD
|
||||
# http_id is needed for Tomato routers only
|
||||
# http_id: ABCDEFGHH
|
||||
# For nmap_tracker, only the IP addresses to scan are needed:
|
||||
# hosts: 192.168.1.1/24 # netmask prefix notation or
|
||||
# hosts: 192.168.1.1-255 # address range
|
||||
|
||||
chromecast:
|
||||
|
||||
@@ -71,24 +72,25 @@ device_sun_light_trigger:
|
||||
# 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:
|
||||
Home:
|
||||
- group.living_room
|
||||
- group.kitchen
|
||||
living_room:
|
||||
- light.Bowl
|
||||
- light.Ceiling
|
||||
- light.TV_back_light
|
||||
kitchen:
|
||||
- light.fan_bulb_1
|
||||
- light.fan_bulb_2
|
||||
children:
|
||||
- device_tracker.child_1
|
||||
- device_tracker.child_2
|
||||
default_view:
|
||||
view: yes
|
||||
entities:
|
||||
- group.awesome_people
|
||||
- group.climate
|
||||
|
||||
process:
|
||||
# items are which processes to look for: <entity_id>: <search string within ps>
|
||||
xbmc: XBMC.App
|
||||
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
|
||||
|
||||
example:
|
||||
|
||||
@@ -102,6 +104,7 @@ browser:
|
||||
|
||||
keyboard:
|
||||
|
||||
# https://home-assistant.io/getting-started/automation/
|
||||
automation:
|
||||
- alias: 'Rule 1 Light on in the evening'
|
||||
trigger:
|
||||
@@ -123,7 +126,6 @@ automation:
|
||||
entity_id: group.living_room
|
||||
|
||||
- alias: 'Rule 2 - Away Mode'
|
||||
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
@@ -136,6 +138,14 @@ automation:
|
||||
|
||||
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
|
||||
# Each sensor label should be unique or your sensors might not load correctly.
|
||||
# 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
|
||||
@@ -146,14 +156,6 @@ sensor:
|
||||
arg: '/home'
|
||||
- type: 'disk_use'
|
||||
arg: '/home'
|
||||
- type: 'disk_free'
|
||||
arg: '/'
|
||||
- type: 'memory_use_percent'
|
||||
- type: 'memory_use'
|
||||
- type: 'memory_free'
|
||||
- type: 'processor_use'
|
||||
- type: 'process'
|
||||
arg: 'octave-cli'
|
||||
|
||||
sensor 2:
|
||||
platform: forecast
|
||||
@@ -163,14 +165,6 @@ sensor 2:
|
||||
- precip_type
|
||||
- precip_intensity
|
||||
- temperature
|
||||
- dew_point
|
||||
- wind_speed
|
||||
- wind_bearing
|
||||
- cloud_cover
|
||||
- humidity
|
||||
- pressure
|
||||
- visibility
|
||||
- ozone
|
||||
|
||||
script:
|
||||
# Turns on the bedroom lights and then the living room lights 1 minute later
|
||||
|
||||
432
config/panels/react.html
Normal file
@@ -0,0 +1,432 @@
|
||||
<!--
|
||||
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>
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 232 KiB |
568
docs/swagger.yaml
Normal file
@@ -0,0 +1,568 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: Home Assistant
|
||||
description: Home Assistant REST API
|
||||
version: "1.0.0"
|
||||
# the domain of the service
|
||||
host: localhost:8123
|
||||
|
||||
# array of all schemes that your API supports
|
||||
schemes:
|
||||
- http
|
||||
- https
|
||||
|
||||
securityDefinitions:
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: api_password
|
||||
in: query
|
||||
|
||||
# api_key:
|
||||
# type: apiKey
|
||||
# description: API password
|
||||
# name: x-ha-access
|
||||
# in: header
|
||||
|
||||
# will be prefixed to all paths
|
||||
basePath: /api
|
||||
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
summary: API alive message
|
||||
description: Returns message if API is up and running.
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: API is up and running
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/config:
|
||||
get:
|
||||
summary: API alive message
|
||||
description: Returns the current configuration as JSON.
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: Current configuration
|
||||
schema:
|
||||
$ref: '#/definitions/ApiConfig'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/discovery_info:
|
||||
get:
|
||||
summary: Basic information about Home Assistant instance
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: Basic information
|
||||
schema:
|
||||
$ref: '#/definitions/DiscoveryInfo'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/bootstrap:
|
||||
get:
|
||||
summary: Returns all data needed to bootstrap Home Assistant.
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: Bootstrap information
|
||||
schema:
|
||||
$ref: '#/definitions/BootstrapInfo'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/events:
|
||||
get:
|
||||
summary: Array of event objects.
|
||||
description: Returns an array of event objects. Each event object contain event name and listener count.
|
||||
tags:
|
||||
- Events
|
||||
responses:
|
||||
200:
|
||||
description: Events
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Event'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/services:
|
||||
get:
|
||||
summary: Array of service objects.
|
||||
description: Returns an array of service objects. Each object contains the domain and which services it contains.
|
||||
tags:
|
||||
- Services
|
||||
responses:
|
||||
200:
|
||||
description: Services
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Service'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/history:
|
||||
get:
|
||||
summary: Array of state changes in the past.
|
||||
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
|
||||
tags:
|
||||
- State
|
||||
responses:
|
||||
200:
|
||||
description: State changes
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/History'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/states:
|
||||
get:
|
||||
summary: Array of state objects.
|
||||
description: |
|
||||
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
|
||||
tags:
|
||||
- State
|
||||
responses:
|
||||
200:
|
||||
description: States
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/states/{entity_id}:
|
||||
get:
|
||||
summary: Specific state object.
|
||||
description: |
|
||||
Returns a state object for specified entity_id.
|
||||
tags:
|
||||
- State
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id of the entity to query
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: State
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
404:
|
||||
description: Not found
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
post:
|
||||
description: |
|
||||
Updates or creates the current state of an entity.
|
||||
tags:
|
||||
- State
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id to set the state of
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/State'
|
||||
responses:
|
||||
200:
|
||||
description: State of existing entity was set
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
201:
|
||||
description: State of new entity was set
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
headers:
|
||||
location:
|
||||
type: string
|
||||
description: location of the new entity
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/error_log:
|
||||
get:
|
||||
summary: Error log
|
||||
description: |
|
||||
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
|
||||
tags:
|
||||
- Core
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
200:
|
||||
description: Plain text error log
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/camera_proxy/camera.{entity_id}:
|
||||
get:
|
||||
summary: Camera image.
|
||||
description: |
|
||||
Returns the data (image) from the specified camera entity_id.
|
||||
tags:
|
||||
- Camera
|
||||
produces:
|
||||
- image/jpeg
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id of the camera to query
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Camera image
|
||||
schema:
|
||||
type: file
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/events/{event_type}:
|
||||
post:
|
||||
description: |
|
||||
Fires an event with event_type
|
||||
tags:
|
||||
- Events
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: event_type
|
||||
in: path
|
||||
description: event_type to fire event with
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/EventData'
|
||||
responses:
|
||||
200:
|
||||
description: Response message
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/services/{domain}/{service}:
|
||||
post:
|
||||
description: |
|
||||
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
|
||||
tags:
|
||||
- Services
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: domain
|
||||
in: path
|
||||
description: domain of the service
|
||||
required: true
|
||||
type: string
|
||||
- name: service
|
||||
in: path
|
||||
description: service to call
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/ServiceData'
|
||||
responses:
|
||||
200:
|
||||
description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/template:
|
||||
post:
|
||||
description: |
|
||||
Render a Home Assistant template.
|
||||
tags:
|
||||
- Template
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- text/plain
|
||||
parameters:
|
||||
- $ref: '#/parameters/Template'
|
||||
responses:
|
||||
200:
|
||||
description: Returns the rendered template in plain text.
|
||||
schema:
|
||||
type: string
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/event_forwarding:
|
||||
post:
|
||||
description: |
|
||||
Setup event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- $ref: '#/parameters/EventForwarding'
|
||||
responses:
|
||||
200:
|
||||
description: It will return a message if event forwarding was setup successful.
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
delete:
|
||||
description: |
|
||||
Cancel event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- $ref: '#/parameters/EventForwarding'
|
||||
responses:
|
||||
200:
|
||||
description: It will return a message if event forwarding was cancelled successful.
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/stream:
|
||||
get:
|
||||
summary: Server-sent events
|
||||
description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer.
|
||||
tags:
|
||||
- Core
|
||||
- Events
|
||||
produces:
|
||||
- text/event-stream
|
||||
parameters:
|
||||
- name: restrict
|
||||
in: query
|
||||
description: comma-separated list of event_types to filter
|
||||
required: false
|
||||
type: string
|
||||
responses:
|
||||
default:
|
||||
description: Stream of events
|
||||
schema:
|
||||
type: object
|
||||
x-events:
|
||||
state_changed:
|
||||
type: object
|
||||
properties:
|
||||
entity_id:
|
||||
type: string
|
||||
old_state:
|
||||
$ref: '#/definitions/State'
|
||||
new_state:
|
||||
$ref: '#/definitions/State'
|
||||
definitions:
|
||||
ApiConfig:
|
||||
type: object
|
||||
properties:
|
||||
components:
|
||||
type: array
|
||||
description: List of component types
|
||||
items:
|
||||
type: string
|
||||
description: Component type
|
||||
latitude:
|
||||
type: number
|
||||
format: float
|
||||
description: Latitude of Home Assistant server
|
||||
longitude:
|
||||
type: number
|
||||
format: float
|
||||
description: Longitude of Home Assistant server
|
||||
location_name:
|
||||
type: string
|
||||
unit_system:
|
||||
type: string
|
||||
description: The system for measurement units
|
||||
time_zone:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
DiscoveryInfo:
|
||||
type: object
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
location_name:
|
||||
type: string
|
||||
requires_api_password:
|
||||
type: boolean
|
||||
version:
|
||||
type: string
|
||||
BootstrapInfo:
|
||||
type: object
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/definitions/ApiConfig'
|
||||
events:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Event'
|
||||
services:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Service'
|
||||
states:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
Event:
|
||||
type: object
|
||||
properties:
|
||||
event:
|
||||
type: string
|
||||
listener_count:
|
||||
type: integer
|
||||
Service:
|
||||
type: object
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
services:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/DomainService'
|
||||
DomainService:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
fields:
|
||||
type: object
|
||||
description: Object with service fields that can be called
|
||||
State:
|
||||
type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: '#/definitions/StateAttributes'
|
||||
state:
|
||||
type: string
|
||||
entity_id:
|
||||
type: string
|
||||
last_changed:
|
||||
type: string
|
||||
format: date-time
|
||||
StateAttributes:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
History:
|
||||
allOf:
|
||||
- $ref: '#/definitions/State'
|
||||
- type: object
|
||||
properties:
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
Message:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
parameters:
|
||||
State:
|
||||
name: body
|
||||
in: body
|
||||
description: State parameter
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- state
|
||||
properties:
|
||||
attributes:
|
||||
$ref: '#/definitions/StateAttributes'
|
||||
state:
|
||||
type: string
|
||||
EventData:
|
||||
name: body
|
||||
in: body
|
||||
description: event_data
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
ServiceData:
|
||||
name: body
|
||||
in: body
|
||||
description: service_data
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
Template:
|
||||
name: body
|
||||
in: body
|
||||
description: Template to render
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- template
|
||||
properties:
|
||||
template:
|
||||
description: Jinja2 template string
|
||||
type: string
|
||||
EventForwarding:
|
||||
name: body
|
||||
in: body
|
||||
description: Event Forwarding parameter
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- host
|
||||
- api_password
|
||||
properties:
|
||||
host:
|
||||
type: string
|
||||
api_password:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
@@ -4,11 +4,11 @@ from __future__ import print_function
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
from homeassistant.const import (
|
||||
__version__,
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
|
||||
def validate_python():
|
||||
def validate_python() -> None:
|
||||
"""Validate we're running the right Python version."""
|
||||
major, minor = sys.version_info[:2]
|
||||
req_major, req_minor = REQUIRED_PYTHON_VER
|
||||
@@ -29,7 +29,7 @@ def validate_python():
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_config_path(config_dir):
|
||||
def ensure_config_path(config_dir: str) -> None:
|
||||
"""Validate the configuration directory."""
|
||||
import homeassistant.config as config_util
|
||||
lib_dir = os.path.join(config_dir, 'deps')
|
||||
@@ -58,7 +58,7 @@ def ensure_config_path(config_dir):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_config_file(config_dir):
|
||||
def ensure_config_file(config_dir: str) -> str:
|
||||
"""Ensure configuration file exists."""
|
||||
import homeassistant.config as config_util
|
||||
config_path = config_util.ensure_config_exists(config_dir)
|
||||
@@ -70,7 +70,7 @@ def ensure_config_file(config_dir):
|
||||
return config_path
|
||||
|
||||
|
||||
def get_arguments():
|
||||
def get_arguments() -> argparse.Namespace:
|
||||
"""Get parsed passed in arguments."""
|
||||
import homeassistant.config as config_util
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -111,22 +111,14 @@ def get_arguments():
|
||||
type=int,
|
||||
default=None,
|
||||
help='Enables daily log rotation and keeps up to the specified days')
|
||||
parser.add_argument(
|
||||
'--install-osx',
|
||||
action='store_true',
|
||||
help='Installs as a service on OS X and loads on boot.')
|
||||
parser.add_argument(
|
||||
'--uninstall-osx',
|
||||
action='store_true',
|
||||
help='Uninstalls from OS X.')
|
||||
parser.add_argument(
|
||||
'--restart-osx',
|
||||
action='store_true',
|
||||
help='Restarts on OS X.')
|
||||
parser.add_argument(
|
||||
'--runner',
|
||||
action='store_true',
|
||||
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
|
||||
parser.add_argument(
|
||||
'--script',
|
||||
nargs=argparse.REMAINDER,
|
||||
help='Run one of the embedded scripts')
|
||||
if os.name == "posix":
|
||||
parser.add_argument(
|
||||
'--daemon',
|
||||
@@ -135,12 +127,12 @@ def get_arguments():
|
||||
|
||||
arguments = parser.parse_args()
|
||||
if os.name != "posix" or arguments.debug or arguments.runner:
|
||||
arguments.daemon = False
|
||||
setattr(arguments, 'daemon', False)
|
||||
|
||||
return arguments
|
||||
|
||||
|
||||
def daemonize():
|
||||
def daemonize() -> None:
|
||||
"""Move current process to daemon process."""
|
||||
# Create first fork
|
||||
pid = os.fork()
|
||||
@@ -165,7 +157,7 @@ def daemonize():
|
||||
os.dup2(outfd.fileno(), sys.stderr.fileno())
|
||||
|
||||
|
||||
def check_pid(pid_file):
|
||||
def check_pid(pid_file: str) -> None:
|
||||
"""Check that HA is not already running."""
|
||||
# Check pid file
|
||||
try:
|
||||
@@ -187,7 +179,7 @@ def check_pid(pid_file):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def write_pid(pid_file):
|
||||
def write_pid(pid_file: str) -> None:
|
||||
"""Create a PID File."""
|
||||
pid = os.getpid()
|
||||
try:
|
||||
@@ -197,47 +189,7 @@ def write_pid(pid_file):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def install_osx():
|
||||
"""Setup to run via launchd on OS X."""
|
||||
with os.popen('which hass') as inp:
|
||||
hass_path = inp.read().strip()
|
||||
|
||||
with os.popen('whoami') as inp:
|
||||
user = inp.read().strip()
|
||||
|
||||
cwd = os.path.dirname(__file__)
|
||||
template_path = os.path.join(cwd, 'startup', 'launchd.plist')
|
||||
|
||||
with open(template_path, 'r', encoding='utf-8') as inp:
|
||||
plist = inp.read()
|
||||
|
||||
plist = plist.replace("$HASS_PATH$", hass_path)
|
||||
plist = plist.replace("$USER$", user)
|
||||
|
||||
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
|
||||
|
||||
try:
|
||||
with open(path, 'w', encoding='utf-8') as outp:
|
||||
outp.write(plist)
|
||||
except IOError as err:
|
||||
print('Unable to write to ' + path, err)
|
||||
return
|
||||
|
||||
os.popen('launchctl load -w -F ' + path)
|
||||
|
||||
print("Home Assistant has been installed. \
|
||||
Open it here: http://localhost:8123")
|
||||
|
||||
|
||||
def uninstall_osx():
|
||||
"""Unload from launchd on OS X."""
|
||||
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
|
||||
os.popen('launchctl unload ' + path)
|
||||
|
||||
print("Home Assistant has been uninstalled.")
|
||||
|
||||
|
||||
def closefds_osx(min_fd, max_fd):
|
||||
def closefds_osx(min_fd: int, max_fd: int) -> None:
|
||||
"""Make sure file descriptors get closed when we restart.
|
||||
|
||||
We cannot call close on guarded fds, and we cannot easily test which fds
|
||||
@@ -255,7 +207,7 @@ def closefds_osx(min_fd, max_fd):
|
||||
pass
|
||||
|
||||
|
||||
def cmdline():
|
||||
def cmdline() -> List[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if sys.argv[0].endswith('/__main__.py'):
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
@@ -263,16 +215,17 @@ def cmdline():
|
||||
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir, args):
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
args: argparse.Namespace) -> Optional[int]:
|
||||
"""Setup HASS and run."""
|
||||
from homeassistant import bootstrap
|
||||
|
||||
# Run a simple daemon runner process on Windows to handle restarts
|
||||
if os.name == 'nt' and '--runner' not in sys.argv:
|
||||
args = cmdline() + ['--runner']
|
||||
nt_args = cmdline() + ['--runner']
|
||||
while True:
|
||||
try:
|
||||
subprocess.check_call(args)
|
||||
subprocess.check_call(nt_args)
|
||||
sys.exit(0)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
if exc.returncode != RESTART_EXIT_CODE:
|
||||
@@ -294,7 +247,7 @@ def setup_and_run_hass(config_dir, args):
|
||||
log_rotate_days=args.log_rotate_days)
|
||||
|
||||
if hass is None:
|
||||
return
|
||||
return None
|
||||
|
||||
if args.open_ui:
|
||||
def open_browser(event):
|
||||
@@ -305,14 +258,13 @@ def setup_and_run_hass(config_dir, args):
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||
|
||||
print('Starting Home-Assistant')
|
||||
hass.start()
|
||||
exit_code = int(hass.block_till_stopped())
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
def try_to_restart():
|
||||
def try_to_restart() -> None:
|
||||
"""Attempt to clean up state and start a new homeassistant instance."""
|
||||
# Things should be mostly shut down already at this point, now just try
|
||||
# to clean up things that may have been left behind.
|
||||
@@ -321,33 +273,18 @@ def try_to_restart():
|
||||
# Count remaining threads, ideally there should only be one non-daemonized
|
||||
# thread left (which is us). Nothing we really do with it, but it might be
|
||||
# useful when debugging shutdown/restart issues.
|
||||
nthreads = sum(thread.isAlive() and not thread.isDaemon()
|
||||
for thread in threading.enumerate())
|
||||
if nthreads > 1:
|
||||
sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads))
|
||||
try:
|
||||
nthreads = sum(thread.is_alive() and not thread.daemon
|
||||
for thread in threading.enumerate())
|
||||
if nthreads > 1:
|
||||
sys.stderr.write(
|
||||
"Found {} non-daemonic threads.\n".format(nthreads))
|
||||
|
||||
# Send terminate signal to all processes in our process group which
|
||||
# should be any children that have not themselves changed the process
|
||||
# group id. Don't bother if couldn't even call setpgid.
|
||||
if hasattr(os, 'setpgid'):
|
||||
sys.stderr.write("Signalling child processes to terminate...\n")
|
||||
os.kill(0, signal.SIGTERM)
|
||||
|
||||
# wait for child processes to terminate
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
if os.waitpid(0, os.WNOHANG) == (0, 0):
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
elif os.name == 'nt':
|
||||
# Maybe one of the following will work, but how do we indicate which
|
||||
# processes are our children if there is no process group?
|
||||
# os.kill(0, signal.CTRL_C_EVENT)
|
||||
# os.kill(0, signal.CTRL_BREAK_EVENT)
|
||||
pass
|
||||
# Somehow we sometimes seem to trigger an assertion in the python threading
|
||||
# module. It seems we find threads that have no associated OS level thread
|
||||
# which are not marked as stopped at the python level.
|
||||
except AssertionError:
|
||||
sys.stderr.write("Failed to count non-daemonic threads.\n")
|
||||
|
||||
# Try to not leave behind open filedescriptors with the emphasis on try.
|
||||
try:
|
||||
@@ -369,29 +306,19 @@ def try_to_restart():
|
||||
os.execv(args[0], args)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
if args.script is not None:
|
||||
from homeassistant import scripts
|
||||
return scripts.run(args.script)
|
||||
|
||||
config_dir = os.path.join(os.getcwd(), args.config)
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# OS X launchd functions
|
||||
if args.install_osx:
|
||||
install_osx()
|
||||
return 0
|
||||
if args.uninstall_osx:
|
||||
uninstall_osx()
|
||||
return 0
|
||||
if args.restart_osx:
|
||||
uninstall_osx()
|
||||
# A small delay is needed on some systems to let the unload finish.
|
||||
time.sleep(0.5)
|
||||
install_osx()
|
||||
return 0
|
||||
|
||||
# Daemon functions
|
||||
if args.pid_file:
|
||||
check_pid(args.pid_file)
|
||||
@@ -400,13 +327,6 @@ def main():
|
||||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
# Create new process group if we can
|
||||
if hasattr(os, 'setpgid'):
|
||||
try:
|
||||
os.setpgid(0, 0)
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
exit_code = setup_and_run_hass(config_dir, args)
|
||||
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||
try_to_restart()
|
||||
|
||||
@@ -3,30 +3,26 @@
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from threading import RLock
|
||||
|
||||
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
|
||||
import homeassistant.components.group as group
|
||||
import homeassistant.config as config_util
|
||||
from homeassistant.components import group, persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.util.location as loc_util
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.const import (
|
||||
CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME,
|
||||
CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__)
|
||||
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.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SETUP_LOCK = RLock()
|
||||
@@ -37,7 +33,8 @@ ATTR_COMPONENT = 'component'
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
|
||||
|
||||
def setup_component(hass, domain, config=None):
|
||||
def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies."""
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
@@ -60,7 +57,8 @@ def setup_component(hass, domain, config=None):
|
||||
return True
|
||||
|
||||
|
||||
def _handle_requirements(hass, component, name):
|
||||
def _handle_requirements(hass: core.HomeAssistant, component,
|
||||
name: str) -> bool:
|
||||
"""Install the requirements for a component."""
|
||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||
return True
|
||||
@@ -74,9 +72,10 @@ def _handle_requirements(hass, component, name):
|
||||
return True
|
||||
|
||||
|
||||
def _setup_component(hass, domain, config):
|
||||
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
||||
"""Setup a component for Home Assistant."""
|
||||
# pylint: disable=too-many-return-statements,too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
|
||||
@@ -104,7 +103,7 @@ def _setup_component(hass, domain, config):
|
||||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, domain, config)
|
||||
_log_exception(ex, domain, config)
|
||||
return False
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
@@ -114,7 +113,7 @@ def _setup_component(hass, domain, config):
|
||||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, domain, p_config)
|
||||
_log_exception(ex, domain, p_config)
|
||||
return False
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
@@ -135,8 +134,8 @@ def _setup_component(hass, domain, config):
|
||||
try:
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.MultipleInvalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, '{}.{}'
|
||||
.format(domain, p_name), p_validated)
|
||||
_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated)
|
||||
return False
|
||||
|
||||
platforms.append(p_validated)
|
||||
@@ -154,9 +153,15 @@ def _setup_component(hass, domain, config):
|
||||
_CURRENT_SETUP.append(domain)
|
||||
|
||||
try:
|
||||
if not component.setup(hass, config):
|
||||
result = component.setup(hass, config)
|
||||
if result is False:
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
return False
|
||||
elif result is not True:
|
||||
_LOGGER.error('component %s did not return boolean if setup '
|
||||
'was successful. Disabling component.', domain)
|
||||
loader.set_component(domain, None)
|
||||
return False
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error during setup of component %s', domain)
|
||||
return False
|
||||
@@ -176,7 +181,8 @@ def _setup_component(hass, domain, config):
|
||||
return True
|
||||
|
||||
|
||||
def prepare_setup_platform(hass, config, domain, platform_name):
|
||||
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) -> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup."""
|
||||
_ensure_loader_prepared(hass)
|
||||
|
||||
@@ -208,15 +214,15 @@ def prepare_setup_platform(hass, config, domain, platform_name):
|
||||
return platform
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir):
|
||||
"""Add local library to Python Path."""
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
|
||||
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
verbose=False, skip_pip=False,
|
||||
log_rotate_days=None):
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
hass: Optional[core.HomeAssistant]=None,
|
||||
config_dir: Optional[str]=None,
|
||||
enable_log: bool=True,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=False,
|
||||
log_rotate_days: Any=None) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a config dict.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
@@ -231,13 +237,12 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA(
|
||||
core_config))
|
||||
except vol.MultipleInvalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
|
||||
conf_util.process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
_log_exception(ex, 'homeassistant', core_config)
|
||||
return None
|
||||
|
||||
process_ha_config_upgrade(hass)
|
||||
conf_util.process_ha_config_upgrade(hass)
|
||||
|
||||
if enable_log:
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
@@ -262,9 +267,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
|
||||
return hass
|
||||
|
||||
persistent_notification.setup(hass, config)
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
@@ -278,8 +284,11 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
return hass
|
||||
|
||||
|
||||
def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
|
||||
log_rotate_days=None):
|
||||
def from_config_file(config_path: str,
|
||||
hass: Optional[core.HomeAssistant]=None,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=True,
|
||||
log_rotate_days: Any=None):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
@@ -296,7 +305,7 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
try:
|
||||
config_dict = config_util.load_yaml_config_file(config_path)
|
||||
config_dict = conf_util.load_yaml_config_file(config_path)
|
||||
except HomeAssistantError:
|
||||
return None
|
||||
|
||||
@@ -304,7 +313,8 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
|
||||
skip_pip=skip_pip)
|
||||
|
||||
|
||||
def enable_logging(hass, verbose=False, log_rotate_days=None):
|
||||
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
log_rotate_days=None) -> None:
|
||||
"""Setup the logging."""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
@@ -355,101 +365,32 @@ def enable_logging(hass, verbose=False, log_rotate_days=None):
|
||||
'Unable to setup error log %s (access denied)', err_log_path)
|
||||
|
||||
|
||||
def process_ha_config_upgrade(hass):
|
||||
"""Upgrade config if necessary."""
|
||||
version_path = hass.config.path('.HA_VERSION')
|
||||
|
||||
try:
|
||||
with open(version_path, 'rt') as inp:
|
||||
conf_version = inp.readline().strip()
|
||||
except FileNotFoundError:
|
||||
# Last version to not have this file
|
||||
conf_version = '0.7.7'
|
||||
|
||||
if conf_version == __version__:
|
||||
return
|
||||
|
||||
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
|
||||
__version__)
|
||||
|
||||
# This was where dependencies were installed before v0.18
|
||||
# Probably should keep this around until ~v0.20.
|
||||
lib_path = hass.config.path('lib')
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
lib_path = hass.config.path('deps')
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
with open(version_path, 'wt') as outp:
|
||||
outp.write(__version__)
|
||||
|
||||
|
||||
def process_ha_core_config(hass, config):
|
||||
"""Process the [homeassistant] section from the config."""
|
||||
hac = hass.config
|
||||
|
||||
def set_time_zone(time_zone_str):
|
||||
"""Helper method to set time zone."""
|
||||
if time_zone_str is None:
|
||||
return
|
||||
|
||||
time_zone = date_util.get_time_zone(time_zone_str)
|
||||
|
||||
if time_zone:
|
||||
hac.time_zone = time_zone
|
||||
date_util.set_default_time_zone(time_zone)
|
||||
else:
|
||||
_LOGGER.error('Received invalid time zone %s', time_zone_str)
|
||||
|
||||
for key, attr in ((CONF_LATITUDE, 'latitude'),
|
||||
(CONF_LONGITUDE, 'longitude'),
|
||||
(CONF_NAME, 'location_name')):
|
||||
if key in config:
|
||||
setattr(hac, attr, config[key])
|
||||
|
||||
if CONF_TIME_ZONE in config:
|
||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||
|
||||
for entity_id, attrs in config.get(CONF_CUSTOMIZE).items():
|
||||
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
|
||||
|
||||
if CONF_TEMPERATURE_UNIT in config:
|
||||
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
|
||||
|
||||
# If we miss some of the needed values, auto detect them
|
||||
if None not in (
|
||||
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
|
||||
return
|
||||
|
||||
_LOGGER.warning('Incomplete core config. Auto detecting location and '
|
||||
'temperature unit')
|
||||
|
||||
info = loc_util.detect_location_info()
|
||||
|
||||
if info is None:
|
||||
_LOGGER.error('Could not detect location information')
|
||||
return
|
||||
|
||||
if hac.latitude is None and hac.longitude is None:
|
||||
hac.latitude = info.latitude
|
||||
hac.longitude = info.longitude
|
||||
|
||||
if hac.temperature_unit is None:
|
||||
if info.use_fahrenheit:
|
||||
hac.temperature_unit = TEMP_FAHRENHEIT
|
||||
else:
|
||||
hac.temperature_unit = TEMP_CELSIUS
|
||||
|
||||
if hac.location_name is None:
|
||||
hac.location_name = info.city
|
||||
|
||||
if hac.time_zone is None:
|
||||
set_time_zone(info.time_zone)
|
||||
|
||||
|
||||
def _ensure_loader_prepared(hass):
|
||||
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
||||
"""Ensure Home Assistant loader is prepared."""
|
||||
if not loader.PREPARED:
|
||||
loader.prepare(hass)
|
||||
|
||||
|
||||
def _log_exception(ex, domain, config):
|
||||
"""Generate log exception for config validation."""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||
.format(ex.path[-1], domain, domain,
|
||||
'->'.join('%s' % m for m in ex.path))
|
||||
else:
|
||||
message += humanize_error(config, ex)
|
||||
|
||||
if hasattr(config, '__line__'):
|
||||
message += " (See {}:{})".format(config.__config_file__,
|
||||
config.__line__ or '?')
|
||||
|
||||
_LOGGER.error(message)
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path."""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
if deps_dir not in sys.path:
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
return deps_dir
|
||||
|
||||
@@ -11,7 +11,6 @@ import itertools as it
|
||||
import logging
|
||||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
@@ -19,6 +18,8 @@ from homeassistant.const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
"""Load up the module to call the is_on method.
|
||||
@@ -33,7 +34,7 @@ def is_on(hass, entity_id=None):
|
||||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = split_entity_id(entity_id)[0]
|
||||
domain = ha.split_entity_id(entity_id)[0]
|
||||
|
||||
module = get_component(domain)
|
||||
|
||||
@@ -73,6 +74,11 @@ def toggle(hass, entity_id=None, **service_data):
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
|
||||
|
||||
|
||||
def reload_core_config(hass):
|
||||
"""Reload the core config."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup general services related to Home Assistant."""
|
||||
def handle_turn_service(service):
|
||||
@@ -88,7 +94,7 @@ def setup(hass, config):
|
||||
|
||||
# Group entity_ids by domain. groupby requires sorted data.
|
||||
by_domain = it.groupby(sorted(entity_ids),
|
||||
lambda item: split_entity_id(item)[0])
|
||||
lambda item: ha.split_entity_id(item)[0])
|
||||
|
||||
for domain, ent_ids in by_domain:
|
||||
# We want to block for all calls and only return when all calls
|
||||
@@ -111,4 +117,21 @@ def setup(hass, config):
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
|
||||
def handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import config as conf_util
|
||||
|
||||
try:
|
||||
path = conf_util.find_config_file(hass.config.config_dir)
|
||||
conf = conf_util.load_yaml_config_file(path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
|
||||
handle_reload_config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -9,7 +9,6 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import verisure
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
|
||||
@@ -21,14 +20,10 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
SCAN_INTERVAL = 30
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
verisure.DISCOVER_ALARMS: 'verisure'
|
||||
}
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_ALARM_DISARM: 'alarm_disarm',
|
||||
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
|
||||
@@ -50,8 +45,7 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
@@ -131,6 +125,11 @@ class AlarmControlPanel(Entity):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Last change triggered by."""
|
||||
return None
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
raise NotImplementedError()
|
||||
@@ -152,5 +151,6 @@ class AlarmControlPanel(Entity):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
ATTR_CHANGED_BY: self.changed_by
|
||||
}
|
||||
return state_attr
|
||||
|
||||
105
homeassistant/components/alarm_control_panel/envisalink.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
EnvisalinkDevice,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_CODE,
|
||||
CONF_PARTITIONNAME,
|
||||
SIGNAL_PARTITION_UPDATE,
|
||||
SIGNAL_KEYPAD_UPDATE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
_code = discovery_info[CONF_CODE]
|
||||
for part_num in _configured_partitions:
|
||||
_device_config_data = PARTITION_SCHEMA(
|
||||
_configured_partitions[part_num])
|
||||
_device = EnvisalinkAlarm(
|
||||
part_num,
|
||||
_device_config_data[CONF_PARTITIONNAME],
|
||||
_code,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Represents the Envisalink-based alarm panel."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, partition_number, alarm_name, code, info, controller):
|
||||
"""Initialize the alarm panel."""
|
||||
from pydispatch import dispatcher
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
_LOGGER.debug('Setting up alarm: ' + alarm_name)
|
||||
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
def _update_callback(self, partition):
|
||||
"""Update HA state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._info['status']['alarm']:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
elif self._info['status']['armed_away']:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif self._info['status']['armed_stay']:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._info['status']['alpha']:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Not possible for us."""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,43 @@
|
||||
alarm_disarm:
|
||||
description: Send the alarm the command for disarm
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to disarm
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to disarm the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
alarm_arm_home:
|
||||
description: Send the alarm the command for arm home
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm home
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm home the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
alarm_arm_away:
|
||||
description: Send the alarm the command for arm away
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm away
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm away the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
alarm_trigger:
|
||||
description: Send the alarm the command for trigger
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to trigger
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
124
homeassistant/components/alarm_control_panel/simplisafe.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Interfaces with SimpliSafe alarm control panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['https://github.com/w1ll1am23/simplisafe-python/archive/'
|
||||
'586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#'
|
||||
'simplisafe-python==0.0.1']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error('Must specify username and password!')
|
||||
return False
|
||||
|
||||
add_devices([SimpliSafeAlarm(
|
||||
config.get('name', "SimpliSafe"),
|
||||
username,
|
||||
password,
|
||||
config.get('code'))])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, name, username, password, code):
|
||||
"""Initialize the SimpliSafe alarm."""
|
||||
from simplisafe import SimpliSafe
|
||||
self.simplisafe = SimpliSafe(username, password)
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._id = self.simplisafe.get_id()
|
||||
status = self.simplisafe.get_state()
|
||||
if status == 'Off':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif status == 'Home':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'Away':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll the SimpliSafe API."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return 'Alarm {}'.format(self._id)
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
self.simplisafe.get_location()
|
||||
status = self.simplisafe.get_state()
|
||||
|
||||
if status == 'Off':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif status == 'Home':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'Away':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
self.simplisafe.set_state('off')
|
||||
_LOGGER.info('SimpliSafe alarm disarming')
|
||||
self.update()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
self.simplisafe.set_state('home')
|
||||
_LOGGER.info('SimpliSafe alarm arming home')
|
||||
self.update()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
return
|
||||
self.simplisafe.set_state('away')
|
||||
_LOGGER.info('SimpliSafe alarm arming away')
|
||||
self.update()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered for %s', state)
|
||||
return check
|
||||
@@ -37,6 +37,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
self._id = device_id
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = int(hub.config.get('code_digits', '4'))
|
||||
self._changed_by = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -58,6 +59,11 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
"""The code format as regex."""
|
||||
return '^\\d{%s}$' % self._digits
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Last change triggered by."""
|
||||
return self._changed_by
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
hub.update_alarms()
|
||||
@@ -72,6 +78,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
_LOGGER.error(
|
||||
'Unknown alarm state %s',
|
||||
hub.alarm_status[self._id].status)
|
||||
self._changed_by = hub.alarm_status[self._id].name
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -7,14 +7,14 @@ https://home-assistant.io/components/alexa/
|
||||
import enum
|
||||
import logging
|
||||
|
||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_CONFIG = {}
|
||||
|
||||
API_ENDPOINT = '/api/alexa'
|
||||
|
||||
@@ -26,80 +26,88 @@ CONF_ACTION = 'action'
|
||||
|
||||
def setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(hass, intent[CONF_ACTION],
|
||||
"Alexa intent {}".format(name))
|
||||
|
||||
_CONFIG.update(intents)
|
||||
|
||||
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
|
||||
hass.wsgi.register_view(AlexaView(hass,
|
||||
config[DOMAIN].get(CONF_INTENTS, {})))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_alexa(handler, path_match, data):
|
||||
"""Handle Alexa."""
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
class AlexaView(HomeAssistantView):
|
||||
"""Handle Alexa requests."""
|
||||
|
||||
req = data.get('request')
|
||||
url = API_ENDPOINT
|
||||
name = 'api:alexa'
|
||||
|
||||
if req is None:
|
||||
_LOGGER.error('Received invalid data from Alexa: %s', data)
|
||||
handler.write_json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
|
||||
req_type = req['type']
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(
|
||||
hass, intent[CONF_ACTION], "Alexa intent {}".format(name))
|
||||
|
||||
if req_type == 'SessionEndedRequest':
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.end_headers()
|
||||
return
|
||||
self.intents = intents
|
||||
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(handler.server.hass, intent)
|
||||
def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
data = request.json
|
||||
|
||||
if req_type == 'LaunchRequest':
|
||||
response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"Hello, and welcome to the future. How may I help?")
|
||||
handler.write_json(response.as_dict())
|
||||
return
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
|
||||
if req_type != 'IntentRequest':
|
||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
||||
return
|
||||
req = data.get('request')
|
||||
|
||||
intent_name = intent['name']
|
||||
config = _CONFIG.get(intent_name)
|
||||
if req is None:
|
||||
_LOGGER.error('Received invalid data from Alexa: %s', data)
|
||||
return self.json_message('Expected request value not received',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
if config is None:
|
||||
_LOGGER.warning('Received unknown intent %s', intent_name)
|
||||
response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"This intent is not yet configured within Home Assistant.")
|
||||
handler.write_json(response.as_dict())
|
||||
return
|
||||
req_type = req['type']
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
if req_type == 'SessionEndedRequest':
|
||||
return None
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(SpeechType[speech['type']], speech['text'])
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(self.hass, intent)
|
||||
|
||||
if card is not None:
|
||||
response.add_card(CardType[card['type']], card['title'],
|
||||
card['content'])
|
||||
if req_type == 'LaunchRequest':
|
||||
response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"Hello, and welcome to the future. How may I help?")
|
||||
return self.json(response)
|
||||
|
||||
if action is not None:
|
||||
action.run(response.variables)
|
||||
if req_type != 'IntentRequest':
|
||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
||||
return self.json_message(
|
||||
'Received unsupported request: {}'.format(req_type),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
handler.write_json(response.as_dict())
|
||||
intent_name = intent['name']
|
||||
config = self.intents.get(intent_name)
|
||||
|
||||
if config is None:
|
||||
_LOGGER.warning('Received unknown intent %s', intent_name)
|
||||
response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"This intent is not yet configured within Home Assistant.")
|
||||
return self.json(response)
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
|
||||
if action is not None:
|
||||
action.run(response.variables)
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(SpeechType[speech['type']], speech['text'])
|
||||
|
||||
if card is not None:
|
||||
response.add_card(CardType[card['type']], card['title'],
|
||||
card['content'])
|
||||
|
||||
return self.json(response)
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
|
||||
@@ -6,23 +6,23 @@ https://home-assistant.io/developers/api/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
import queue
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
||||
from homeassistant.const import (
|
||||
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
|
||||
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
|
||||
HTTP_UNPROCESSABLE_ENTITY, 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_LOG_OUT, URL_API_SERVICES,
|
||||
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_SERVICES,
|
||||
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import TrackStates
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'api'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -35,372 +35,351 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup(hass, config):
|
||||
"""Register the API with the HTTP interface."""
|
||||
# /api - for validation purposes
|
||||
hass.http.register_path('GET', URL_API, _handle_get_api)
|
||||
|
||||
# /api/config
|
||||
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
|
||||
|
||||
# /api/discovery_info
|
||||
hass.http.register_path('GET', URL_API_DISCOVERY_INFO,
|
||||
_handle_get_api_discovery_info,
|
||||
require_auth=False)
|
||||
|
||||
# /api/stream
|
||||
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
|
||||
|
||||
# /api/states
|
||||
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_get_api_states_entity)
|
||||
hass.http.register_path(
|
||||
'POST', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_post_state_entity)
|
||||
hass.http.register_path(
|
||||
'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_post_state_entity)
|
||||
hass.http.register_path(
|
||||
'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_delete_state_entity)
|
||||
|
||||
# /api/events
|
||||
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
|
||||
hass.http.register_path(
|
||||
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
||||
_handle_api_post_events_event)
|
||||
|
||||
# /api/services
|
||||
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
|
||||
hass.http.register_path(
|
||||
'POST',
|
||||
re.compile((r'/api/services/'
|
||||
r'(?P<domain>[a-zA-Z\._0-9]+)/'
|
||||
r'(?P<service>[a-zA-Z\._0-9]+)')),
|
||||
_handle_post_api_services_domain_service)
|
||||
|
||||
# /api/event_forwarding
|
||||
hass.http.register_path(
|
||||
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
|
||||
hass.http.register_path(
|
||||
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
|
||||
|
||||
# /api/components
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_COMPONENTS, _handle_get_api_components)
|
||||
|
||||
# /api/error_log
|
||||
hass.http.register_path('GET', URL_API_ERROR_LOG,
|
||||
_handle_get_api_error_log)
|
||||
|
||||
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
|
||||
|
||||
# /api/template
|
||||
hass.http.register_path('POST', URL_API_TEMPLATE,
|
||||
_handle_post_api_template)
|
||||
hass.wsgi.register_view(APIStatusView)
|
||||
hass.wsgi.register_view(APIEventStream)
|
||||
hass.wsgi.register_view(APIConfigView)
|
||||
hass.wsgi.register_view(APIDiscoveryView)
|
||||
hass.wsgi.register_view(APIStatesView)
|
||||
hass.wsgi.register_view(APIEntityStateView)
|
||||
hass.wsgi.register_view(APIEventListenersView)
|
||||
hass.wsgi.register_view(APIEventView)
|
||||
hass.wsgi.register_view(APIServicesView)
|
||||
hass.wsgi.register_view(APIDomainServicesView)
|
||||
hass.wsgi.register_view(APIEventForwardingView)
|
||||
hass.wsgi.register_view(APIComponentsView)
|
||||
hass.wsgi.register_view(APIErrorLogView)
|
||||
hass.wsgi.register_view(APITemplateView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api(handler, path_match, data):
|
||||
"""Render the debug interface."""
|
||||
handler.write_json_message("API running.")
|
||||
|
||||
|
||||
def _handle_get_api_stream(handler, path_match, data):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
gracefully_closed = False
|
||||
hass = handler.server.hass
|
||||
wfile = handler.wfile
|
||||
write_lock = threading.Lock()
|
||||
block = threading.Event()
|
||||
session_id = None
|
||||
class APIStatusView(HomeAssistantView):
|
||||
"""View to handle Status requests."""
|
||||
|
||||
restrict = data.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',')
|
||||
url = URL_API
|
||||
name = "api:status"
|
||||
|
||||
def write_message(payload):
|
||||
"""Write a message to the output."""
|
||||
with write_lock:
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
def get(self, request):
|
||||
"""Retrieve if API is running."""
|
||||
return self.json_message('API running.')
|
||||
|
||||
try:
|
||||
wfile.write(msg.encode("UTF-8"))
|
||||
wfile.flush()
|
||||
except (IOError, ValueError):
|
||||
# IOError: socket errors
|
||||
# ValueError: raised when 'I/O operation on closed file'
|
||||
block.set()
|
||||
|
||||
def forward_events(event):
|
||||
"""Forward events to the open request."""
|
||||
nonlocal gracefully_closed
|
||||
class APIEventStream(HomeAssistantView):
|
||||
"""View to handle EventStream requests."""
|
||||
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
gracefully_closed = True
|
||||
block.set()
|
||||
return
|
||||
url = URL_API_STREAM
|
||||
name = "api:stream"
|
||||
|
||||
handler.server.sessions.extend_validation(session_id)
|
||||
write_message(json.dumps(event, cls=rem.JSONEncoder))
|
||||
def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
stop_obj = object()
|
||||
to_write = queue.Queue()
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header('Content-type', 'text/event-stream')
|
||||
session_id = handler.set_session_cookie_header()
|
||||
handler.end_headers()
|
||||
restrict = request.args.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||
|
||||
if restrict:
|
||||
for event in restrict:
|
||||
hass.bus.listen(event, forward_events)
|
||||
else:
|
||||
hass.bus.listen(MATCH_ALL, forward_events)
|
||||
def forward_events(event):
|
||||
"""Forward events to the open request."""
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
|
||||
while True:
|
||||
write_message(STREAM_PING_PAYLOAD)
|
||||
if restrict and event.event_type not in restrict:
|
||||
return
|
||||
|
||||
block.wait(STREAM_PING_INTERVAL)
|
||||
_LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event)
|
||||
|
||||
if block.is_set():
|
||||
break
|
||||
if event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
data = stop_obj
|
||||
else:
|
||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||
|
||||
if not gracefully_closed:
|
||||
_LOGGER.info("Found broken event stream to %s, cleaning up",
|
||||
handler.client_address[0])
|
||||
to_write.put(data)
|
||||
|
||||
if restrict:
|
||||
for event in restrict:
|
||||
hass.bus.remove_listener(event, forward_events)
|
||||
else:
|
||||
hass.bus.remove_listener(MATCH_ALL, forward_events)
|
||||
def stream():
|
||||
"""Stream events to response."""
|
||||
self.hass.bus.listen(MATCH_ALL, forward_events)
|
||||
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
|
||||
def _handle_get_api_config(handler, path_match, data):
|
||||
"""Return the Home Assistant configuration."""
|
||||
handler.write_json(handler.server.hass.config.as_dict())
|
||||
# Fire off one message right away to have browsers fire open event
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
while True:
|
||||
try:
|
||||
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
|
||||
|
||||
def _handle_get_api_discovery_info(handler, path_match, data):
|
||||
needs_auth = (handler.server.hass.config.api.api_password is not None)
|
||||
params = {
|
||||
'base_url': handler.server.hass.config.api.base_url,
|
||||
'location_name': handler.server.hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
}
|
||||
handler.write_json(params)
|
||||
if payload is stop_obj:
|
||||
break
|
||||
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
yield msg.encode("UTF-8")
|
||||
except queue.Empty:
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
except GeneratorExit:
|
||||
break
|
||||
|
||||
def _handle_get_api_states(handler, path_match, data):
|
||||
"""Return a dict containing all entity ids and their state."""
|
||||
handler.write_json(handler.server.hass.states.all())
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
self.hass.bus.remove_listener(MATCH_ALL, forward_events)
|
||||
|
||||
return self.Response(stream(), mimetype='text/event-stream')
|
||||
|
||||
def _handle_get_api_states_entity(handler, path_match, data):
|
||||
"""Return the state of a specific entity."""
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
state = handler.server.hass.states.get(entity_id)
|
||||
class APIConfigView(HomeAssistantView):
|
||||
"""View to handle Config requests."""
|
||||
|
||||
if state:
|
||||
handler.write_json(state)
|
||||
else:
|
||||
handler.write_json_message("State does not exist.", HTTP_NOT_FOUND)
|
||||
url = URL_API_CONFIG
|
||||
name = "api:config"
|
||||
|
||||
def get(self, request):
|
||||
"""Get current configuration."""
|
||||
return self.json(self.hass.config.as_dict())
|
||||
|
||||
def _handle_post_state_entity(handler, path_match, data):
|
||||
"""Handle updating the state of an entity.
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
"""
|
||||
entity_id = path_match.group('entity_id')
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
"""View to provide discovery info."""
|
||||
|
||||
try:
|
||||
new_state = data['state']
|
||||
except KeyError:
|
||||
handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
|
||||
return
|
||||
requires_auth = False
|
||||
url = URL_API_DISCOVERY_INFO
|
||||
name = "api:discovery"
|
||||
|
||||
attributes = data['attributes'] if 'attributes' in data else None
|
||||
def get(self, request):
|
||||
"""Get discovery info."""
|
||||
needs_auth = self.hass.config.api.api_password is not None
|
||||
return self.json({
|
||||
'base_url': self.hass.config.api.base_url,
|
||||
'location_name': self.hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
})
|
||||
|
||||
is_new_state = handler.server.hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
handler.server.hass.states.set(entity_id, new_state, attributes)
|
||||
class APIStatesView(HomeAssistantView):
|
||||
"""View to handle States requests."""
|
||||
|
||||
state = handler.server.hass.states.get(entity_id)
|
||||
url = URL_API_STATES
|
||||
name = "api:states"
|
||||
|
||||
status_code = HTTP_CREATED if is_new_state else HTTP_OK
|
||||
def get(self, request):
|
||||
"""Get current states."""
|
||||
return self.json(self.hass.states.all())
|
||||
|
||||
handler.write_json(
|
||||
state.as_dict(),
|
||||
status_code=status_code,
|
||||
location=URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
"""View to handle EntityState requests."""
|
||||
|
||||
def _handle_delete_state_entity(handler, path_match, data):
|
||||
"""Handle request to delete an entity from state machine.
|
||||
url = "/api/states/<entity(exist=False):entity_id>"
|
||||
name = "api:entity-state"
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
"""
|
||||
entity_id = path_match.group('entity_id')
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve state of entity."""
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
if handler.server.hass.states.remove(entity_id):
|
||||
handler.write_json_message(
|
||||
"Entity not found", HTTP_NOT_FOUND)
|
||||
else:
|
||||
handler.write_json_message(
|
||||
"Entity removed", HTTP_OK)
|
||||
def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
try:
|
||||
new_state = request.json['state']
|
||||
except KeyError:
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = request.json.get('attributes')
|
||||
force_update = request.json.get('force_update', False)
|
||||
|
||||
def _handle_get_api_events(handler, path_match, data):
|
||||
"""Handle getting overview of event listeners."""
|
||||
handler.write_json(events_json(handler.server.hass))
|
||||
is_new_state = self.hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
self.hass.states.set(entity_id, new_state, attributes, force_update)
|
||||
|
||||
def _handle_api_post_events_event(handler, path_match, event_data):
|
||||
"""Handle firing of an event.
|
||||
# Read the state back for our response
|
||||
resp = self.json(self.hass.states.get(entity_id))
|
||||
|
||||
This handles the following paths: /api/events/<event_type>
|
||||
if is_new_state:
|
||||
resp.status_code = HTTP_CREATED
|
||||
|
||||
Events from /api are threated as remote events.
|
||||
"""
|
||||
event_type = path_match.group('event_type')
|
||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
handler.write_json_message(
|
||||
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
return resp
|
||||
|
||||
event_origin = ha.EventOrigin.remote
|
||||
def delete(self, request, entity_id):
|
||||
"""Remove entity."""
|
||||
if self.hass.states.remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
"""View to handle EventListeners requests."""
|
||||
|
||||
handler.server.hass.bus.fire(event_type, event_data, event_origin)
|
||||
url = URL_API_EVENTS
|
||||
name = "api:event-listeners"
|
||||
|
||||
handler.write_json_message("Event {} fired.".format(event_type))
|
||||
def get(self, request):
|
||||
"""Get event listeners."""
|
||||
return self.json(events_json(self.hass))
|
||||
|
||||
|
||||
def _handle_get_api_services(handler, path_match, data):
|
||||
"""Handle getting overview of services."""
|
||||
handler.write_json(services_json(handler.server.hass))
|
||||
class APIEventView(HomeAssistantView):
|
||||
"""View to handle Event requests."""
|
||||
|
||||
url = '/api/events/<event_type>'
|
||||
name = "api:event"
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_services_domain_service(handler, path_match, data):
|
||||
"""Handle calling a service.
|
||||
def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
event_data = request.json
|
||||
|
||||
This handles the following paths: /api/services/<domain>/<service>
|
||||
"""
|
||||
domain = path_match.group('domain')
|
||||
service = path_match.group('service')
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
return self.json_message('Event data should be a JSON object',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
with TrackStates(handler.server.hass) as changed_states:
|
||||
handler.server.hass.services.call(domain, service, data, True)
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
|
||||
handler.write_json(changed_states)
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_event_forward(handler, path_match, data):
|
||||
"""Handle adding an event forwarding target."""
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
except KeyError:
|
||||
handler.write_json_message(
|
||||
"No host or api_password received.", HTTP_BAD_REQUEST)
|
||||
return
|
||||
return self.json_message("Event {} fired.".format(event_type))
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
handler.write_json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
class APIServicesView(HomeAssistantView):
|
||||
"""View to handle Services requests."""
|
||||
|
||||
if not api.validate_api():
|
||||
handler.write_json_message(
|
||||
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
|
||||
if handler.server.event_forwarder is None:
|
||||
handler.server.event_forwarder = \
|
||||
rem.EventForwarder(handler.server.hass)
|
||||
def get(self, request):
|
||||
"""Get registered services."""
|
||||
return self.json(services_json(self.hass))
|
||||
|
||||
handler.server.event_forwarder.connect(api)
|
||||
|
||||
handler.write_json_message("Event forwarding setup.")
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
"""View to handle DomainServices requests."""
|
||||
|
||||
url = "/api/services/<domain>/<service>"
|
||||
name = "api:domain-services"
|
||||
|
||||
def _handle_delete_api_event_forward(handler, path_match, data):
|
||||
"""Handle deleting an event forwarding target."""
|
||||
try:
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
|
||||
return
|
||||
def post(self, request, domain, service):
|
||||
"""Call a service.
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
handler.write_json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
Returns a list of changed states.
|
||||
"""
|
||||
with TrackStates(self.hass) as changed_states:
|
||||
self.hass.services.call(domain, service, request.json, True)
|
||||
|
||||
if handler.server.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
return self.json(changed_states)
|
||||
|
||||
handler.server.event_forwarder.disconnect(api)
|
||||
|
||||
handler.write_json_message("Event forwarding cancelled.")
|
||||
class APIEventForwardingView(HomeAssistantView):
|
||||
"""View to handle EventForwarding requests."""
|
||||
|
||||
url = URL_API_EVENT_FORWARD
|
||||
name = "api:event-forward"
|
||||
event_forwarder = None
|
||||
|
||||
def _handle_get_api_components(handler, path_match, data):
|
||||
"""Return all the loaded components."""
|
||||
handler.write_json(handler.server.hass.config.components)
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
data = request.json
|
||||
if data is None:
|
||||
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)
|
||||
|
||||
def _handle_get_api_error_log(handler, path_match, data):
|
||||
"""Return the logged errors for this session."""
|
||||
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
|
||||
False)
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
if not api.validate_api():
|
||||
return self.json_message("Unable to validate API.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
def _handle_post_api_log_out(handler, path_match, data):
|
||||
"""Log user out."""
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.destroy_session()
|
||||
handler.end_headers()
|
||||
if self.event_forwarder is None:
|
||||
self.event_forwarder = rem.EventForwarder(self.hass)
|
||||
|
||||
self.event_forwarder.connect(api)
|
||||
|
||||
def _handle_post_api_template(handler, path_match, data):
|
||||
"""Log user out."""
|
||||
template_string = data.get('template', '')
|
||||
return self.json_message("Event forwarding setup.")
|
||||
|
||||
try:
|
||||
rendered = template.render(handler.server.hass, template_string)
|
||||
def delete(self, request):
|
||||
"""Remove event forwarer."""
|
||||
data = request.json
|
||||
if data is None:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
|
||||
handler.end_headers()
|
||||
handler.wfile.write(rendered.encode('utf-8'))
|
||||
except TemplateError as e:
|
||||
handler.write_json_message(str(e), HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
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.disconnect(api)
|
||||
|
||||
return self.json_message("Event forwarding cancelled.")
|
||||
|
||||
|
||||
class APIComponentsView(HomeAssistantView):
|
||||
"""View to handle Components requests."""
|
||||
|
||||
url = URL_API_COMPONENTS
|
||||
name = "api:components"
|
||||
|
||||
def get(self, request):
|
||||
"""Get current loaded components."""
|
||||
return self.json(self.hass.config.components)
|
||||
|
||||
|
||||
class APIErrorLogView(HomeAssistantView):
|
||||
"""View to handle ErrorLog requests."""
|
||||
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error-log"
|
||||
|
||||
def get(self, request):
|
||||
"""Serve error log."""
|
||||
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
|
||||
|
||||
|
||||
class APITemplateView(HomeAssistantView):
|
||||
"""View to handle requests."""
|
||||
|
||||
url = URL_API_TEMPLATE
|
||||
name = "api:template"
|
||||
|
||||
def post(self, request):
|
||||
"""Render a template."""
|
||||
try:
|
||||
return template.render(self.hass, request.json['template'],
|
||||
request.json.get('variables'))
|
||||
except TemplateError as ex:
|
||||
return self.json_message('Error rendering template: {}'.format(ex),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
def services_json(hass):
|
||||
|
||||
@@ -9,8 +9,6 @@ import logging
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF)
|
||||
from homeassistant.components import (
|
||||
bloomsky, mysensors, zwave, vera, wemo, wink)
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
|
||||
DOMAIN = 'binary_sensor'
|
||||
@@ -35,22 +33,11 @@ SENSOR_CLASSES = [
|
||||
'vibration', # On means vibration detected, Off means no vibration
|
||||
]
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
bloomsky.DISCOVER_BINARY_SENSORS: 'bloomsky',
|
||||
mysensors.DISCOVER_BINARY_SENSORS: 'mysensors',
|
||||
zwave.DISCOVER_BINARY_SENSORS: 'zwave',
|
||||
vera.DISCOVER_BINARY_SENSORS: 'vera',
|
||||
wemo.DISCOVER_BINARY_SENSORS: 'wemo',
|
||||
wink.DISCOVER_BINARY_SENSORS: 'wink'
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
|
||||
63
homeassistant/components/binary_sensor/enocean.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Support for EnOcean binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.enocean/
|
||||
"""
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components import enocean
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
DEPENDENCIES = ["enocean"]
|
||||
|
||||
CONF_ID = "id"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Binary Sensor platform fo EnOcean."""
|
||||
dev_id = config.get(CONF_ID, None)
|
||||
devname = config.get(CONF_NAME, "EnOcean binary sensor")
|
||||
add_devices([EnOceanBinarySensor(dev_id, devname)])
|
||||
|
||||
|
||||
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
"""Representation of EnOcean binary sensors such as wall switches."""
|
||||
|
||||
def __init__(self, dev_id, devname):
|
||||
"""Initialize the EnOcean binary sensor."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.stype = "listener"
|
||||
self.dev_id = dev_id
|
||||
self.which = -1
|
||||
self.onoff = -1
|
||||
self.devname = devname
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The default name for the binary sensor."""
|
||||
return self.devname
|
||||
|
||||
def value_changed(self, value, value2):
|
||||
"""Fire an event with the data that have changed.
|
||||
|
||||
This method is called when there is an incoming packet associated
|
||||
with this platform.
|
||||
"""
|
||||
self.update_ha_state()
|
||||
if value2 == 0x70:
|
||||
self.which = 0
|
||||
self.onoff = 0
|
||||
elif value2 == 0x50:
|
||||
self.which = 0
|
||||
self.onoff = 1
|
||||
elif value2 == 0x30:
|
||||
self.which = 1
|
||||
self.onoff = 0
|
||||
elif value2 == 0x10:
|
||||
self.which = 1
|
||||
self.onoff = 1
|
||||
self.hass.bus.fire('button_pressed', {"id": self.dev_id,
|
||||
'pushed': value,
|
||||
'which': self.which,
|
||||
'onoff': self.onoff})
|
||||
71
homeassistant/components/binary_sensor/envisalink.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Support for Envisalink 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.envisalink/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
ZONE_SCHEMA,
|
||||
CONF_ZONENAME,
|
||||
CONF_ZONETYPE,
|
||||
EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup Envisalink binary sensor devices."""
|
||||
_configured_zones = discovery_info['zones']
|
||||
for zone_num in _configured_zones:
|
||||
_device_config_data = ZONE_SCHEMA(_configured_zones[zone_num])
|
||||
_device = EnvisalinkBinarySensor(
|
||||
zone_num,
|
||||
_device_config_data[CONF_ZONENAME],
|
||||
_device_config_data[CONF_ZONETYPE],
|
||||
EVL_CONTROLLER.alarm_state['zone'][zone_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""Representation of an Envisalink binary sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
self._zone_type = zone_type
|
||||
self._zone_number = zone_number
|
||||
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
EnvisalinkDevice.__init__(self, zone_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault']
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._info['status']['open']
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
def _update_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self.update_ha_state()
|
||||
100
homeassistant/components/binary_sensor/homematic.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Support for Homematic binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematic/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
SENSOR_TYPES_CLASS = {
|
||||
"Remote": None,
|
||||
"ShutterContact": "opening",
|
||||
"Smoke": "smoke",
|
||||
"SmokeV2": "smoke",
|
||||
"Motion": "motion",
|
||||
"MotionV2": "motion",
|
||||
"RemoteMotion": None
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the Homematic binary sensor platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
return homematic.setup_hmdevice_discovery_helper(HMBinarySensor,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
|
||||
"""Representation of a binary Homematic device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
if not self.available:
|
||||
return False
|
||||
return bool(self._hm_get_state())
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
if not self.available:
|
||||
return None
|
||||
|
||||
# If state is MOTION (RemoteMotion works only)
|
||||
if self._state == "MOTION":
|
||||
return "motion"
|
||||
return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None)
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the HM Object as this HA type."""
|
||||
from pyhomematic.devicetypes.sensors import HMBinarySensor\
|
||||
as pyHMBinarySensor
|
||||
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
# check if the Homematic device correct for this HA device
|
||||
if not isinstance(self._hmdevice, pyHMBinarySensor):
|
||||
_LOGGER.critical("This %s can't be use as binary", self._name)
|
||||
return False
|
||||
|
||||
# if exists user value?
|
||||
if self._state and self._state not in self._hmdevice.BINARYNODE:
|
||||
_LOGGER.critical("This %s have no binary with %s", self._name,
|
||||
self._state)
|
||||
return False
|
||||
|
||||
# only check and give a warning to the user
|
||||
if self._state is None and len(self._hmdevice.BINARYNODE) > 1:
|
||||
_LOGGER.critical("%s have multiple binary params. It use all "
|
||||
"binary nodes as one. Possible param values: %s",
|
||||
self._name, str(self._hmdevice.BINARYNODE))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data struct (self._data) from the Homematic metadata."""
|
||||
super()._init_data_struct()
|
||||
|
||||
# object have 1 binary
|
||||
if self._state is None and len(self._hmdevice.BINARYNODE) == 1:
|
||||
for value in self._hmdevice.BINARYNODE:
|
||||
self._state = value
|
||||
|
||||
# add state to data struct
|
||||
if self._state:
|
||||
_LOGGER.debug("%s init datastruct with main node '%s'", self._name,
|
||||
self._state)
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
24
homeassistant/components/binary_sensor/knx.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Contains functionality to use a KNX group address as a binary.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.knx/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.knx import (
|
||||
KNXConfig, KNXGroupAddress)
|
||||
|
||||
DEPENDENCIES = ["knx"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup the KNX binary sensor platform."""
|
||||
add_entities([
|
||||
KNXSwitch(hass, KNXConfig(config))
|
||||
])
|
||||
|
||||
|
||||
class KNXSwitch(KNXGroupAddress, BinarySensorDevice):
|
||||
"""Representation of a KNX binary sensor device."""
|
||||
|
||||
pass
|
||||
@@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup nx584 sensors."""
|
||||
"""Setup nx584 binary sensor platform."""
|
||||
from nx584 import client as nx584_client
|
||||
|
||||
host = config.get('host', 'localhost:5007')
|
||||
|
||||
@@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
rest.update()
|
||||
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch Rest data')
|
||||
_LOGGER.error('Unable to fetch REST data')
|
||||
return False
|
||||
|
||||
add_devices([RestBinarySensor(
|
||||
@@ -57,6 +57,7 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
self._name = name
|
||||
self._sensor_class = sensor_class
|
||||
self._state = False
|
||||
self._previous_data = None
|
||||
self._value_template = value_template
|
||||
self.update()
|
||||
|
||||
@@ -77,9 +78,14 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
if self._value_template is not None:
|
||||
self.rest.data = template.render_with_possible_json_value(
|
||||
response = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
return bool(int(self.rest.data))
|
||||
|
||||
try:
|
||||
return bool(int(response))
|
||||
except ValueError:
|
||||
return {"true": True, "on": True, "open": True,
|
||||
"yes": True}.get(response.lower(), False)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from REST API and updates the state."""
|
||||
|
||||
@@ -9,11 +9,12 @@ import logging
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
ENTITY_ID_FORMAT,
|
||||
SENSOR_CLASSES)
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE
|
||||
from homeassistant.core import EVENT_STATE_CHANGED
|
||||
from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE,
|
||||
ATTR_ENTITY_ID, MATCH_ALL)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.util import slugify
|
||||
|
||||
CONF_SENSORS = 'sensors'
|
||||
@@ -52,13 +53,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
'Missing %s for sensor %s', CONF_VALUE_TEMPLATE, device)
|
||||
continue
|
||||
|
||||
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
|
||||
|
||||
sensors.append(
|
||||
BinarySensorTemplate(
|
||||
hass,
|
||||
device,
|
||||
friendly_name,
|
||||
sensor_class,
|
||||
value_template)
|
||||
value_template,
|
||||
entity_ids)
|
||||
)
|
||||
if not sensors:
|
||||
_LOGGER.error('No sensors added')
|
||||
@@ -73,7 +77,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, device, friendly_name, sensor_class,
|
||||
value_template):
|
||||
value_template, entity_ids):
|
||||
"""Initialize the Template binary sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
@@ -85,12 +89,12 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
self.update()
|
||||
|
||||
def template_bsensor_event_listener(event):
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.update_ha_state(True)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED,
|
||||
template_bsensor_event_listener)
|
||||
track_state_change(hass, entity_ids,
|
||||
template_bsensor_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -6,9 +6,6 @@ https://home-assistant.io/components/binary_sensor.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice)
|
||||
from homeassistant.components.vera import (
|
||||
@@ -34,30 +31,6 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
self._state = False
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
if self.vera_device.has_battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
||||
if self.vera_device.is_armable:
|
||||
armed = self.vera_device.is_armed
|
||||
attr[ATTR_ARMED] = 'True' if armed else 'False'
|
||||
|
||||
if self.vera_device.is_trippable:
|
||||
last_tripped = self.vera_device.last_trip
|
||||
if last_tripped is not None:
|
||||
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
|
||||
attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat()
|
||||
else:
|
||||
attr[ATTR_LAST_TRIP_TIME] = None
|
||||
tripped = self.vera_device.is_tripped
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
||||
|
||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
|
||||
@@ -5,24 +5,28 @@ For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/sensor.wink/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
"opened": "opening",
|
||||
"brightness": "light",
|
||||
"vibration": "vibration",
|
||||
"loudness": "sound"
|
||||
"loudness": "sound",
|
||||
"liquid_detected": "moisture"
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink platform."""
|
||||
"""Setup the Wink binary sensor platform."""
|
||||
import pywink
|
||||
|
||||
if discovery_info is None:
|
||||
@@ -40,17 +44,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
|
||||
for key in pywink.get_keys():
|
||||
add_devices([WinkBinarySensorDevice(key)])
|
||||
|
||||
class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink sensor."""
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
self.wink = wink
|
||||
super().__init__(wink)
|
||||
wink = get_component('wink')
|
||||
self._unit_of_measurement = self.wink.UNIT
|
||||
self._battery = self.wink.battery_level
|
||||
self.capability = self.wink.capability()
|
||||
|
||||
def _pubnub_update(self, message, channel):
|
||||
if 'data' in message:
|
||||
json_data = json.dumps(message.get('data'))
|
||||
else:
|
||||
json_data = message
|
||||
self.wink.pubnub_update(json.loads(json_data))
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
@@ -60,6 +75,8 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
||||
return self.wink.vibration_boolean()
|
||||
elif self.capability == "brightness":
|
||||
return self.wink.brightness_boolean()
|
||||
elif self.capability == "liquid_detected":
|
||||
return self.wink.liquid_boolean()
|
||||
else:
|
||||
return self.wink.state()
|
||||
|
||||
@@ -67,35 +84,3 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this wink sensor."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor if any."""
|
||||
return self.wink.name()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def update(self):
|
||||
"""Update state of the sensor."""
|
||||
self.wink.update_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
||||
@@ -12,7 +12,7 @@ DEPENDENCIES = ["zigbee"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
"""Setup the ZigBee binary sensor platform."""
|
||||
add_entities([
|
||||
ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))
|
||||
])
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
import datetime
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
@@ -31,7 +32,7 @@ DEVICE_MAPPINGS = {
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Z-Wave platform for sensors."""
|
||||
"""Setup the Z-Wave platform for binary sensors."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
@@ -61,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
|
||||
|
||||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
"""Representation of a binary sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, sensor_class):
|
||||
@@ -93,11 +94,12 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_ha_state()
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
||||
"""Representation of a stateless sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, sensor_value, sensor_class, hass, re_arm_sec=60):
|
||||
|
||||
@@ -9,9 +9,8 @@ from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = "bloomsky"
|
||||
@@ -23,10 +22,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# no point in polling the API more frequently
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||
|
||||
DISCOVER_SENSORS = 'bloomsky.sensors'
|
||||
DISCOVER_BINARY_SENSORS = 'bloomsky.binary_sensor'
|
||||
DISCOVER_CAMERAS = 'bloomsky.camera'
|
||||
|
||||
|
||||
# pylint: disable=unused-argument,too-few-public-methods
|
||||
def setup(hass, config):
|
||||
@@ -45,11 +40,8 @@ def setup(hass, config):
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
for component, discovery_service in (
|
||||
('camera', DISCOVER_CAMERAS), ('sensor', DISCOVER_SENSORS),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS)):
|
||||
discovery.discover(hass, discovery_service, component=component,
|
||||
hass_config=config)
|
||||
for component in 'camera', 'binary_sensor', 'sensor':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ ATTR_URL = 'url'
|
||||
ATTR_URL_DEFAULT = 'https://www.google.com'
|
||||
|
||||
SERVICE_BROWSE_URL_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url,
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -6,96 +6,36 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components import bloomsky
|
||||
from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
SCAN_INTERVAL = 30
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
bloomsky.DISCOVER_CAMERAS: 'bloomsky',
|
||||
}
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
|
||||
|
||||
MULTIPART_BOUNDARY = '--jpgboundary'
|
||||
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
"""Setup the camera component."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.wsgi.register_view(CameraImageView(hass, component.entities))
|
||||
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
|
||||
|
||||
component.setup(config)
|
||||
|
||||
def _proxy_camera_image(handler, path_match, data):
|
||||
"""Serve the camera image via the HA server."""
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
camera = component.entities.get(entity_id)
|
||||
|
||||
if camera is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
response = camera.camera_image()
|
||||
|
||||
if response is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.write_content(response)
|
||||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_proxy_camera_image)
|
||||
|
||||
def _proxy_camera_mjpeg_stream(handler, path_match, data):
|
||||
"""Proxy the camera image as an mjpeg stream via the HA server."""
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
camera = component.entities.get(entity_id)
|
||||
|
||||
if camera is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
try:
|
||||
camera.is_streaming = True
|
||||
camera.update_ha_state()
|
||||
camera.mjpeg_stream(handler)
|
||||
|
||||
except (requests.RequestException, IOError):
|
||||
camera.is_streaming = False
|
||||
camera.update_ha_state()
|
||||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_proxy_camera_mjpeg_stream)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -106,6 +46,11 @@ class Camera(Entity):
|
||||
"""Initialize a camera."""
|
||||
self.is_streaming = False
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
"""Access token for this camera."""
|
||||
return str(id(self))
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No need to poll cameras."""
|
||||
@@ -114,7 +59,7 @@ class Camera(Entity):
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Return a link to the camera feed as entity picture."""
|
||||
return ENTITY_IMAGE_URL.format(self.entity_id)
|
||||
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token)
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
@@ -135,32 +80,33 @@ class Camera(Entity):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
def write_string(text):
|
||||
"""Helper method to write a string to the stream."""
|
||||
handler.request.sendall(bytes(text + '\r\n', 'utf-8'))
|
||||
def stream():
|
||||
"""Stream images as mjpeg stream."""
|
||||
try:
|
||||
last_image = None
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
|
||||
write_string('HTTP/1.1 200 OK')
|
||||
write_string('Content-type: multipart/x-mixed-replace; '
|
||||
'boundary={}'.format(MULTIPART_BOUNDARY))
|
||||
write_string('')
|
||||
write_string(MULTIPART_BOUNDARY)
|
||||
if img_bytes is not None and img_bytes != last_image:
|
||||
yield bytes(
|
||||
'--jpegboundary\r\n'
|
||||
'Content-Type: image/jpeg\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n'
|
||||
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
last_image = img_bytes
|
||||
|
||||
if img_bytes is None:
|
||||
continue
|
||||
time.sleep(0.5)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
|
||||
write_string('Content-length: {}'.format(len(img_bytes)))
|
||||
write_string('Content-type: image/jpeg')
|
||||
write_string('')
|
||||
handler.request.sendall(img_bytes)
|
||||
write_string('')
|
||||
write_string(MULTIPART_BOUNDARY)
|
||||
|
||||
time.sleep(0.5)
|
||||
return response(
|
||||
stream(),
|
||||
content_type=('multipart/x-mixed-replace; '
|
||||
'boundary=--jpegboundary')
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -175,7 +121,9 @@ class Camera(Entity):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Camera state attributes."""
|
||||
attr = {}
|
||||
attr = {
|
||||
'access_token': self.access_token,
|
||||
}
|
||||
|
||||
if self.model:
|
||||
attr['model_name'] = self.model
|
||||
@@ -184,3 +132,60 @@ class Camera(Entity):
|
||||
attr['brand'] = self.brand
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
class CameraView(HomeAssistantView):
|
||||
"""Base CameraView."""
|
||||
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass, entities):
|
||||
"""Initialize a basic camera view."""
|
||||
super().__init__(hass)
|
||||
self.entities = entities
|
||||
|
||||
def get(self, request, entity_id):
|
||||
"""Start a get request."""
|
||||
camera = self.entities.get(entity_id)
|
||||
|
||||
if camera is None:
|
||||
return self.Response(status=404)
|
||||
|
||||
authenticated = (request.authenticated or
|
||||
request.args.get('token') == camera.access_token)
|
||||
|
||||
if not authenticated:
|
||||
return self.Response(status=401)
|
||||
|
||||
return self.handle(camera)
|
||||
|
||||
def handle(self, camera):
|
||||
"""Hanlde the camera request."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CameraImageView(CameraView):
|
||||
"""Camera view to serve an image."""
|
||||
|
||||
url = "/api/camera_proxy/<entity(domain=camera):entity_id>"
|
||||
name = "api:camera:image"
|
||||
|
||||
def handle(self, camera):
|
||||
"""Serve camera image."""
|
||||
response = camera.camera_image()
|
||||
|
||||
if response is None:
|
||||
return self.Response(status=500)
|
||||
|
||||
return self.Response(response)
|
||||
|
||||
|
||||
class CameraMjpegStream(CameraView):
|
||||
"""Camera View to serve an MJPEG stream."""
|
||||
|
||||
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>"
|
||||
name = "api:camera:stream"
|
||||
|
||||
def handle(self, camera):
|
||||
"""Serve camera image."""
|
||||
return camera.mjpeg_stream(self.Response)
|
||||
|
||||
@@ -18,7 +18,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class DemoCamera(Camera):
|
||||
"""A Demo camera."""
|
||||
"""The representation of a Demo camera."""
|
||||
|
||||
def __init__(self, name):
|
||||
"""Initialize demo camera component."""
|
||||
|
||||
75
homeassistant/components/camera/ffmpeg.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Support for Cameras with FFmpeg as decoder.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.ffmpeg/
|
||||
"""
|
||||
import logging
|
||||
from contextlib import closing
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.camera.mjpeg import extract_image_from_mjpeg
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
||||
|
||||
REQUIREMENTS = ["ha-ffmpeg==0.4"]
|
||||
|
||||
CONF_INPUT = 'input'
|
||||
CONF_FFMPEG_BIN = 'ffmpeg_bin'
|
||||
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "ffmpeg",
|
||||
vol.Optional(CONF_NAME, default="FFmpeg"): cv.string,
|
||||
vol.Required(CONF_INPUT): cv.string,
|
||||
vol.Optional(CONF_FFMPEG_BIN, default="ffmpeg"): cv.string,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
add_devices_callback([FFmpegCamera(config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
"""An implementation of an FFmpeg camera."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize a FFmpeg camera."""
|
||||
super().__init__()
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._input = config.get(CONF_INPUT)
|
||||
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
self._ffmpeg_bin = config.get(CONF_FFMPEG_BIN)
|
||||
|
||||
def _ffmpeg_stream(self):
|
||||
"""Return a FFmpeg process object."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
ffmpeg = CameraMjpeg(self._ffmpeg_bin)
|
||||
ffmpeg.open_camera(self._input, extra_cmd=self._extra_arguments)
|
||||
return ffmpeg
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
with closing(self._ffmpeg_stream()) as stream:
|
||||
return extract_image_from_mjpeg(stream)
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
stream = self._ffmpeg_stream()
|
||||
return response(
|
||||
stream,
|
||||
mimetype='multipart/x-mixed-replace;boundary=ffserver',
|
||||
direct_passthrough=True
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
@@ -49,7 +49,7 @@ class FoscamCamera(Camera):
|
||||
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)
|
||||
response = requests.get(self._snap_picture_url, timeout=10)
|
||||
|
||||
return response.content
|
||||
|
||||
|
||||
@@ -43,13 +43,14 @@ class GenericCamera(Camera):
|
||||
try:
|
||||
response = requests.get(
|
||||
self._still_image_url,
|
||||
auth=HTTPBasicAuth(self._username, self._password))
|
||||
auth=HTTPBasicAuth(self._username, self._password),
|
||||
timeout=10)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
response = requests.get(self._still_image_url)
|
||||
response = requests.get(self._still_image_url, timeout=10)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return None
|
||||
|
||||
53
homeassistant/components/camera/local_file.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Camera that loads a picture from a local file."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Camera."""
|
||||
# check for missing required configuration variable
|
||||
if config.get("file_path") is None:
|
||||
_LOGGER.error("Missing required variable: file_path")
|
||||
return False
|
||||
|
||||
setup_config = (
|
||||
{
|
||||
"name": config.get("name", "Local File"),
|
||||
"file_path": config.get("file_path")
|
||||
}
|
||||
)
|
||||
|
||||
# check filepath given is readable
|
||||
if not os.access(setup_config["file_path"], os.R_OK):
|
||||
_LOGGER.error("file path is not readable")
|
||||
return False
|
||||
|
||||
add_devices([
|
||||
LocalFile(setup_config)
|
||||
])
|
||||
|
||||
|
||||
class LocalFile(Camera):
|
||||
"""Local camera."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
"""Initialize Local File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._name = device_info["name"]
|
||||
self._config = device_info
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
with open(self._config["file_path"], 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
@@ -11,7 +11,6 @@ import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.const import HTTP_OK
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
@@ -29,6 +28,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
add_devices_callback([MjpegCamera(config)])
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
"""Take in a MJPEG stream object, return the jpg from it."""
|
||||
data = b''
|
||||
for chunk in stream:
|
||||
data += chunk
|
||||
jpg_start = data.find(b'\xff\xd8')
|
||||
jpg_end = data.find(b'\xff\xd9')
|
||||
if jpg_start != -1 and jpg_end != -1:
|
||||
jpg = data[jpg_start:jpg_end + 2]
|
||||
return jpg
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class MjpegCamera(Camera):
|
||||
"""An implementation of an IP camera that is reachable over a URL."""
|
||||
@@ -47,40 +58,23 @@ class MjpegCamera(Camera):
|
||||
return requests.get(self._mjpeg_url,
|
||||
auth=HTTPBasicAuth(self._username,
|
||||
self._password),
|
||||
stream=True)
|
||||
stream=True, timeout=10)
|
||||
else:
|
||||
return requests.get(self._mjpeg_url,
|
||||
stream=True)
|
||||
return requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
def process_response(response):
|
||||
"""Take in a response object, return the jpg from it."""
|
||||
data = b''
|
||||
for chunk in response.iter_content(1024):
|
||||
data += chunk
|
||||
jpg_start = data.find(b'\xff\xd8')
|
||||
jpg_end = data.find(b'\xff\xd9')
|
||||
if jpg_start != -1 and jpg_end != -1:
|
||||
jpg = data[jpg_start:jpg_end + 2]
|
||||
return jpg
|
||||
|
||||
with closing(self.camera_stream()) as response:
|
||||
return process_response(response)
|
||||
return extract_image_from_mjpeg(response.iter_content(1024))
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
response = self.camera_stream()
|
||||
content_type = response.headers[CONTENT_TYPE_HEADER]
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header(CONTENT_TYPE_HEADER, content_type)
|
||||
handler.end_headers()
|
||||
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if not chunk:
|
||||
break
|
||||
handler.wfile.write(chunk)
|
||||
stream = self.camera_stream()
|
||||
return response(
|
||||
stream.iter_content(chunk_size=1024),
|
||||
mimetype=stream.headers[CONTENT_TYPE_HEADER],
|
||||
direct_passthrough=True
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
104
homeassistant/components/camera/netatmo.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Support for the Netatmo Welcome camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_HOME = 'home'
|
||||
ATTR_CAMERAS = 'cameras'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup access to Netatmo Welcome cameras."""
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME, None)
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if ATTR_CAMERAS in config:
|
||||
if camera_name not in config[ATTR_CAMERAS]:
|
||||
continue
|
||||
add_devices_callback([WelcomeCamera(data, camera_name, home)])
|
||||
|
||||
|
||||
class WelcomeCamera(Camera):
|
||||
"""Representation of the images published from Welcome camera."""
|
||||
|
||||
def __init__(self, data, camera_name, home):
|
||||
"""Setup for access to the BloomSky camera images."""
|
||||
super(WelcomeCamera, self).__init__()
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls(
|
||||
camera=camera_name
|
||||
)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
try:
|
||||
if self._localurl:
|
||||
response = requests.get('{0}/live/snapshot_720.jpg'.format(
|
||||
self._localurl), timeout=10)
|
||||
else:
|
||||
response = requests.get('{0}/live/snapshot_720.jpg'.format(
|
||||
self._vpnurl), timeout=10)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Welcome VPN url changed: %s', error)
|
||||
self._data.update()
|
||||
(self._vpnurl, self._localurl) = \
|
||||
self._data.welcomedata.cameraUrls(camera=self._camera_name)
|
||||
return None
|
||||
return response.content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this Netatmo Welcome device."""
|
||||
return self._name
|
||||
|
||||
|
||||
class WelcomeData(object):
|
||||
"""Get the latest data from NetAtmo."""
|
||||
|
||||
def __init__(self, auth, home=None):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.welcomedata = None
|
||||
self.camera_names = []
|
||||
self.home = home
|
||||
|
||||
def get_camera_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.update()
|
||||
if not self.home:
|
||||
for home in self.welcomedata.cameras:
|
||||
for camera in self.welcomedata.cameras[home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
else:
|
||||
for camera in self.welcomedata.cameras[self.home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
return self.camera_names
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.welcomedata = lnetatmo.WelcomeData(self.auth)
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Camera platform that has a Raspberry Pi camera."""
|
||||
"""
|
||||
Camera platform that has a Raspberry Pi camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.rpi_camera/
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
@@ -43,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class RaspberryCamera(Camera):
|
||||
"""Raspberry Pi camera."""
|
||||
"""Representation of a Raspberry Pi camera."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
"""Initialize Raspberry Pi camera component."""
|
||||
|
||||
@@ -12,7 +12,7 @@ import requests
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.8']
|
||||
REQUIREMENTS = ['uvcclient==0.9.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,13 +45,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error('Unable to connect to NVR: %s', str(ex))
|
||||
return False
|
||||
|
||||
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid'
|
||||
# Filter out airCam models, which are not supported in the latest
|
||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||
cameras = [camera for camera in cameras
|
||||
if 'airCam' not in nvrconn.get_camera(camera['uuid'])['model']]
|
||||
cameras = [
|
||||
camera for camera in cameras
|
||||
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
|
||||
|
||||
add_devices([UnifiVideoCamera(nvrconn,
|
||||
camera['uuid'],
|
||||
camera[identifier],
|
||||
camera['name'])
|
||||
for camera in cameras])
|
||||
return True
|
||||
@@ -110,12 +112,17 @@ class UnifiVideoCamera(Camera):
|
||||
dict(name=self._name))
|
||||
password = 'ubnt'
|
||||
|
||||
if self._nvr.server_version >= (3, 2, 0):
|
||||
client_cls = uvc_camera.UVCCameraClientV320
|
||||
else:
|
||||
client_cls = uvc_camera.UVCCameraClient
|
||||
|
||||
camera = None
|
||||
for addr in addrs:
|
||||
try:
|
||||
camera = uvc_camera.UVCCameraClient(addr,
|
||||
caminfo['username'],
|
||||
password)
|
||||
camera = client_cls(addr,
|
||||
caminfo['username'],
|
||||
password)
|
||||
camera.login()
|
||||
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
|
||||
dict(name=self._name, addr=addr))
|
||||
|
||||
@@ -8,7 +8,7 @@ the user has submitted configuration information.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_TIME_CHANGED
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
DOMAIN = "configurator"
|
||||
@@ -118,6 +118,7 @@ class Configurator(object):
|
||||
data = {
|
||||
ATTR_CONFIGURE_ID: request_id,
|
||||
ATTR_FIELDS: fields,
|
||||
ATTR_FRIENDLY_NAME: name,
|
||||
}
|
||||
|
||||
data.update({
|
||||
|
||||
@@ -27,7 +27,7 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.8.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.11.1']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@@ -67,8 +67,8 @@ def setup(hass, config):
|
||||
}, blocking=True)
|
||||
|
||||
else:
|
||||
logger.error(
|
||||
'Got unsupported command %s from text %s', command, text)
|
||||
logger.error('Got unsupported command %s from text %s',
|
||||
command, text)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PROCESS, process,
|
||||
schema=SERVICE_PROCESS_SCHEMA)
|
||||
|
||||
@@ -37,6 +37,7 @@ def setup(hass, config):
|
||||
"""Setup a demo environment."""
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
|
||||
config.setdefault(ha.DOMAIN, {})
|
||||
config.setdefault(DOMAIN, {})
|
||||
@@ -59,6 +60,11 @@ def setup(hass, config):
|
||||
demo_config[component] = {CONF_PLATFORM: 'demo'}
|
||||
bootstrap.setup_component(hass, component, demo_config)
|
||||
|
||||
# Setup example persistent notification
|
||||
persistent_notification.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'))
|
||||
@@ -67,7 +73,9 @@ def setup(hass, config):
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'scene.romantic_lights'])
|
||||
group.Group(hass, 'bedroom', [lights[0], switches[1], media_players[0]])
|
||||
group.Group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance'])
|
||||
group.Group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
|
||||
group.Group(hass, 'doors', [
|
||||
@@ -145,6 +153,17 @@ def setup(hass, config):
|
||||
{'input_boolean': {'notify': {'icon': 'mdi:car',
|
||||
'initial': False,
|
||||
'name': 'Notify Anne Therese is home'}}})
|
||||
|
||||
# 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'}}})
|
||||
|
||||
# Set up weblink
|
||||
bootstrap.setup_component(
|
||||
hass, 'weblink',
|
||||
|
||||
@@ -12,12 +12,13 @@ import os
|
||||
import threading
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.components import discovery, group, zone
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@@ -26,6 +27,7 @@ from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||
DOMAIN = "device_tracker"
|
||||
DEPENDENCIES = ['zone']
|
||||
|
||||
@@ -61,7 +63,7 @@ ATTR_GPS = 'gps'
|
||||
ATTR_BATTERY = 'battery'
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
discovery.SERVICE_NETGEAR: 'netgear',
|
||||
SERVICE_NETGEAR: 'netgear',
|
||||
}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -94,8 +96,11 @@ def setup(hass, config):
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
|
||||
conf = config.get(DOMAIN, {})
|
||||
if isinstance(conf, list) and len(conf) > 0:
|
||||
conf = conf[0]
|
||||
|
||||
# Config can be an empty list. In that case, substitute a dict
|
||||
if isinstance(conf, list):
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
|
||||
consider_home = timedelta(
|
||||
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
|
||||
DEFAULT_CONSIDER_HOME))
|
||||
@@ -193,7 +198,7 @@ class DeviceTracker(object):
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = str(dev_id).lower()
|
||||
dev_id = cv.slug(str(dev_id).lower())
|
||||
device = self.devices.get(dev_id)
|
||||
|
||||
if device:
|
||||
@@ -372,12 +377,16 @@ def load_config(path, hass, consider_home, home_range):
|
||||
"""Load devices from YAML configuration file."""
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
return [
|
||||
Device(hass, consider_home, home_range, device.get('track', False),
|
||||
str(dev_id).lower(), str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
for dev_id, device in load_yaml_config_file(path).items()]
|
||||
try:
|
||||
return [
|
||||
Device(hass, consider_home, home_range, device.get('track', False),
|
||||
str(dev_id).lower(), str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
for dev_id, device in load_yaml_config_file(path).items()]
|
||||
except HomeAssistantError:
|
||||
# When YAML file could not be loaded/did not contain a dict
|
||||
return []
|
||||
|
||||
|
||||
def setup_scanner_platform(hass, config, scanner, see_device):
|
||||
|
||||
@@ -6,8 +6,10 @@ https://home-assistant.io/components/device_tracker.asuswrt/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import telnetlib
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
@@ -28,6 +30,21 @@ _LEASES_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
|
||||
r'(?P<host>([^\s]+))')
|
||||
|
||||
# command to get both 5GHz and 2.4GHz clients
|
||||
_WL_CMD = '{ wl -i eth2 assoclist & wl -i eth1 assoclist ; }'
|
||||
_WL_REGEX = re.compile(
|
||||
r'\w+\s' +
|
||||
r'(?P<mac>(([0-9A-F]{2}[:-]){5}([0-9A-F]{2})))')
|
||||
|
||||
_ARP_CMD = 'arp -n'
|
||||
_ARP_REGEX = re.compile(
|
||||
r'.+\s' +
|
||||
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
|
||||
r'.+\s' +
|
||||
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
|
||||
r'\s' +
|
||||
r'.*')
|
||||
|
||||
_IP_NEIGH_CMD = 'ip neigh'
|
||||
_IP_NEIGH_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
|
||||
@@ -41,24 +58,36 @@ _IP_NEIGH_REGEX = re.compile(
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an ASUS-WRT scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME]},
|
||||
_LOGGER):
|
||||
return None
|
||||
elif CONF_PASSWORD not in config[DOMAIN] and \
|
||||
'ssh_key' not in config[DOMAIN] and \
|
||||
'pub_key' not in config[DOMAIN]:
|
||||
_LOGGER.error('Either a private key or password must be provided')
|
||||
return None
|
||||
|
||||
scanner = AsusWrtDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(object):
|
||||
"""This class queries a router running ASUSWRT firmware."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-branches
|
||||
# Eighth attribute needed for mode (AP mode vs router mode)
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = str(config[CONF_USERNAME])
|
||||
self.password = str(config[CONF_PASSWORD])
|
||||
self.password = str(config.get(CONF_PASSWORD, ''))
|
||||
self.ssh_key = str(config.get('ssh_key', config.get('pub_key', '')))
|
||||
self.protocol = config.get('protocol')
|
||||
self.mode = config.get('mode')
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
@@ -92,7 +121,7 @@ class AsusWrtDeviceScanner(object):
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Checking ARP")
|
||||
_LOGGER.info('Checking ARP')
|
||||
data = self.get_asuswrt_data()
|
||||
if not data:
|
||||
return False
|
||||
@@ -106,21 +135,40 @@ class AsusWrtDeviceScanner(object):
|
||||
|
||||
def ssh_connection(self):
|
||||
"""Retrieve data from ASUSWRT via the ssh protocol."""
|
||||
from pexpect import pxssh
|
||||
from pexpect import pxssh, exceptions
|
||||
|
||||
try:
|
||||
ssh = pxssh.pxssh()
|
||||
ssh.login(self.host, self.username, self.password)
|
||||
if self.ssh_key:
|
||||
ssh.login(self.host, self.username, ssh_key=self.ssh_key)
|
||||
elif self.password:
|
||||
ssh.login(self.host, self.username, self.password)
|
||||
else:
|
||||
_LOGGER.error('No password or private key specified')
|
||||
return None
|
||||
ssh.sendline(_IP_NEIGH_CMD)
|
||||
ssh.prompt()
|
||||
neighbors = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.sendline(_LEASES_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
if self.mode == 'ap':
|
||||
ssh.sendline(_ARP_CMD)
|
||||
ssh.prompt()
|
||||
arp_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.sendline(_WL_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
else:
|
||||
arp_result = ['']
|
||||
ssh.sendline(_LEASES_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.logout()
|
||||
return (neighbors, leases_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
except pxssh.ExceptionPxssh as exc:
|
||||
_LOGGER.exception('Unexpected response from router: %s', exc)
|
||||
return ('', '')
|
||||
_LOGGER.error('Unexpected response from router: %s', exc)
|
||||
return None
|
||||
except exceptions.EOF:
|
||||
_LOGGER.error('Connection refused or no route to host')
|
||||
return None
|
||||
|
||||
def telnet_connection(self):
|
||||
"""Retrieve data from ASUSWRT via the telnet protocol."""
|
||||
@@ -133,50 +181,102 @@ class AsusWrtDeviceScanner(object):
|
||||
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
|
||||
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
|
||||
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
|
||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
|
||||
if self.mode == 'ap':
|
||||
telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
|
||||
arp_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
else:
|
||||
arp_result = ['']
|
||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
return (neighbors, leases_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
return ('', '')
|
||||
_LOGGER.error('Unexpected response from router')
|
||||
return None
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router,"
|
||||
" is telnet enabled?")
|
||||
return ('', '')
|
||||
_LOGGER.error('Connection refused by router, is telnet enabled?')
|
||||
return None
|
||||
except socket.gaierror as exc:
|
||||
_LOGGER.error('Socket exception: %s', exc)
|
||||
return None
|
||||
except OSError as exc:
|
||||
_LOGGER.error('OSError: %s', exc)
|
||||
return None
|
||||
|
||||
def get_asuswrt_data(self):
|
||||
"""Retrieve data from ASUSWRT and return parsed result."""
|
||||
if self.protocol == 'telnet':
|
||||
neighbors, leases_result = self.telnet_connection()
|
||||
if self.protocol == 'ssh':
|
||||
result = self.ssh_connection()
|
||||
elif self.protocol == 'telnet':
|
||||
result = self.telnet_connection()
|
||||
else:
|
||||
neighbors, leases_result = self.ssh_connection()
|
||||
# autodetect protocol
|
||||
result = self.ssh_connection()
|
||||
if result:
|
||||
self.protocol = 'ssh'
|
||||
else:
|
||||
result = self.telnet_connection()
|
||||
if result:
|
||||
self.protocol = 'telnet'
|
||||
|
||||
if not result:
|
||||
return {}
|
||||
|
||||
devices = {}
|
||||
for lease in leases_result:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
if self.mode == 'ap':
|
||||
for lease in result.leases:
|
||||
match = _WL_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse lease row: %s", lease)
|
||||
continue
|
||||
if not match:
|
||||
_LOGGER.warning('Could not parse wl row: %s', lease)
|
||||
continue
|
||||
|
||||
# For leases where the client doesn't set a hostname, ensure it is
|
||||
# blank and not '*', which breaks the entity_id down the line.
|
||||
host = match.group('host')
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
# match mac addresses to IP addresses in ARP table
|
||||
for arp in result.arp:
|
||||
if match.group('mac').lower() in arp.decode('utf-8'):
|
||||
arp_match = _ARP_REGEX.search(arp.decode('utf-8'))
|
||||
if not arp_match:
|
||||
_LOGGER.warning('Could not parse arp row: %s', arp)
|
||||
continue
|
||||
|
||||
for neighbor in neighbors:
|
||||
devices[arp_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': arp_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
else:
|
||||
for lease in result.leases:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
if not match:
|
||||
_LOGGER.warning('Could not parse lease row: %s', lease)
|
||||
continue
|
||||
|
||||
# For leases where the client doesn't set a hostname, ensure it
|
||||
# is blank and not '*', which breaks entity_id down the line.
|
||||
host = match.group('host')
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
for neighbor in result.neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
|
||||
_LOGGER.warning('Could not parse neighbor row: %s', neighbor)
|
||||
continue
|
||||
if match.group('ip') in devices:
|
||||
devices[match.group('ip')]['status'] = match.group('status')
|
||||
|
||||
141
homeassistant/components/device_tracker/bt_home_hub_5.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Support for BT Home Hub 5.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.bt_home_hub_5/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
from urllib.parse import unquote
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Return a BT Home Hub 5 scanner if successful."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST]},
|
||||
_LOGGER):
|
||||
return None
|
||||
scanner = BTHomeHub5DeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class BTHomeHub5DeviceScanner(object):
|
||||
"""This class queries a BT Home Hub 5."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialise the scanner."""
|
||||
_LOGGER.info("Initialising BT Home Hub 5")
|
||||
self.host = config.get(CONF_HOST, '192.168.1.254')
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
|
||||
|
||||
# Test the router is accessible
|
||||
data = _get_homehub_data(self.url)
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return (device for device in self.last_results)
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
# If not initialised and not already scanned and not found.
|
||||
if device not in self.last_results:
|
||||
self._update_info()
|
||||
|
||||
if not self.last_results:
|
||||
return None
|
||||
|
||||
return self.last_results.get(device)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the BT Home Hub 5 is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Scanning")
|
||||
|
||||
data = _get_homehub_data(self.url)
|
||||
|
||||
if not data:
|
||||
_LOGGER.warning('Error scanning devices')
|
||||
return False
|
||||
|
||||
self.last_results = data
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _get_homehub_data(url):
|
||||
"""Retrieve data from BT Home Hub 5 and return parsed result."""
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.exception("Connection to the router timed out")
|
||||
return
|
||||
if response.status_code == 200:
|
||||
return _parse_homehub_response(response.text)
|
||||
else:
|
||||
_LOGGER.error("Invalid response from Home Hub: %s", response)
|
||||
|
||||
|
||||
def _parse_homehub_response(data_str):
|
||||
"""Parse the BT Home Hub 5 data format."""
|
||||
root = ET.fromstring(data_str)
|
||||
|
||||
dirty_json = root.find('known_device_list').get('value')
|
||||
|
||||
# Normalise the JavaScript data to JSON.
|
||||
clean_json = unquote(dirty_json.replace('\'', '\"')
|
||||
.replace('{', '{\"')
|
||||
.replace(':\"', '\":\"')
|
||||
.replace('\",', '\",\"'))
|
||||
|
||||
known_devices = [x for x in json.loads(clean_json) if x]
|
||||
|
||||
devices = {}
|
||||
|
||||
for device in known_devices:
|
||||
name = device.get('name')
|
||||
mac = device.get('mac')
|
||||
|
||||
if _MAC_REGEX.match(mac) or ',' in mac:
|
||||
for mac_addr in mac.split(','):
|
||||
if _MAC_REGEX.match(mac_addr):
|
||||
devices[mac_addr] = name
|
||||
else:
|
||||
devices[mac] = name
|
||||
|
||||
return devices
|
||||
@@ -5,17 +5,28 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.icloud/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT,
|
||||
PLATFORM_SCHEMA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyicloud==0.8.3']
|
||||
REQUIREMENTS = ['pyicloud==0.9.1']
|
||||
|
||||
CONF_INTERVAL = 'interval'
|
||||
DEFAULT_INTERVAL = 8
|
||||
KEEPALIVE_INTERVAL = 4
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): vol.Coerce(str),
|
||||
vol.Required(CONF_PASSWORD): vol.Coerce(str),
|
||||
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1))
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
@@ -23,63 +34,67 @@ def setup_scanner(hass, config, see):
|
||||
from pyicloud import PyiCloudService
|
||||
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
|
||||
|
||||
# Get the username and password from the configuration.
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error('Must specify a username and password')
|
||||
return False
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
_LOGGER.info('Logging into iCloud Account')
|
||||
# Attempt the login to iCloud
|
||||
api = PyiCloudService(username,
|
||||
password,
|
||||
verify=True)
|
||||
api = PyiCloudService(username, password, verify=True)
|
||||
except PyiCloudFailedLoginException as error:
|
||||
_LOGGER.exception('Error logging into iCloud Service: %s', error)
|
||||
return False
|
||||
|
||||
def keep_alive(now):
|
||||
"""Keep authenticating iCloud connection."""
|
||||
"""Keep authenticating iCloud connection.
|
||||
|
||||
The session timeouts if we are not using it so we
|
||||
have to re-authenticate & this will send an email.
|
||||
"""
|
||||
api.authenticate()
|
||||
_LOGGER.info("Authenticate against iCloud")
|
||||
|
||||
track_utc_time_change(hass, keep_alive, second=0)
|
||||
seen_devices = {}
|
||||
|
||||
def update_icloud(now):
|
||||
"""Authenticate against iCloud and scan for devices."""
|
||||
try:
|
||||
# The session timeouts if we are not using it so we
|
||||
# have to re-authenticate. This will send an email.
|
||||
api.authenticate()
|
||||
keep_alive(None)
|
||||
# Loop through every device registered with the iCloud account
|
||||
for device in api.devices:
|
||||
status = device.status()
|
||||
dev_id = slugify(status['name'].replace(' ', '', 99))
|
||||
|
||||
# An entity will not be created by see() when track=false in
|
||||
# 'known_devices.yaml', but we need to see() it at least once
|
||||
entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id))
|
||||
if entity is None and dev_id in seen_devices:
|
||||
continue
|
||||
seen_devices[dev_id] = True
|
||||
|
||||
location = device.location()
|
||||
# If the device has a location add it. If not do nothing
|
||||
if location:
|
||||
see(
|
||||
dev_id=re.sub(r"(\s|\W|')",
|
||||
'',
|
||||
status['name']),
|
||||
dev_id=dev_id,
|
||||
host_name=status['name'],
|
||||
gps=(location['latitude'], location['longitude']),
|
||||
battery=status['batteryLevel']*100,
|
||||
gps_accuracy=location['horizontalAccuracy']
|
||||
)
|
||||
else:
|
||||
# No location found for the device so continue
|
||||
continue
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.info('No iCloud Devices found!')
|
||||
|
||||
track_utc_time_change(
|
||||
hass, update_icloud,
|
||||
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)),
|
||||
second=0
|
||||
)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
|
||||
|
||||
update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
|
||||
# Schedule keepalives between the updates
|
||||
keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL)
|
||||
if x not in update_minutes)
|
||||
|
||||
track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes)
|
||||
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes)
|
||||
|
||||
return True
|
||||
|
||||
@@ -5,95 +5,92 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.locative/
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup an endpoint for the Locative application."""
|
||||
# POST would be semantically better, but that currently does not work
|
||||
# since Locative sends the data as key1=value1&key2=value2
|
||||
# in the request body, while Home Assistant expects json there.
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_LOCATIVE_ENDPOINT,
|
||||
partial(_handle_get_api_locative, hass, see))
|
||||
hass.wsgi.register_view(LocativeView(hass, see))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_locative(hass, see, handler, path_match, data):
|
||||
"""Locative message received."""
|
||||
if not _check_data(handler, data):
|
||||
return
|
||||
class LocativeView(HomeAssistantView):
|
||||
"""View to handle locative requests."""
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
url = "/api/locative"
|
||||
name = "api:locative"
|
||||
|
||||
if direction == 'enter':
|
||||
see(dev_id=device, location_name=location_name)
|
||||
handler.write_text("Setting location to {}".format(location_name))
|
||||
def __init__(self, hass, see):
|
||||
"""Initialize Locative url endpoints."""
|
||||
super().__init__(hass)
|
||||
self.see = see
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = hass.states.get("{}.{}".format(DOMAIN, device))
|
||||
def get(self, request):
|
||||
"""Locative message received as GET."""
|
||||
return self.post(request)
|
||||
|
||||
def post(self, request):
|
||||
"""Locative message received."""
|
||||
# pylint: disable=too-many-return-statements
|
||||
data = request.values
|
||||
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
return ("Latitude and longitude not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'device' not in data:
|
||||
_LOGGER.error("Device id not specified.")
|
||||
return ("Device id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'id' not in data:
|
||||
_LOGGER.error("Location id not specified.")
|
||||
return ("Location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'trigger' not in data:
|
||||
_LOGGER.error("Trigger is not specified.")
|
||||
return ("Trigger is not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
|
||||
if direction == 'enter':
|
||||
self.see(dev_id=device, location_name=location_name)
|
||||
return "Setting location to {}".format(location_name)
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = self.hass.states.get(
|
||||
"{}.{}".format(DOMAIN, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
self.see(dev_id=device, location_name=STATE_NOT_HOME)
|
||||
return "Setting location to not home"
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered
|
||||
# before the previous zone was exited. The enter message will
|
||||
# be sent first, then the exit message will be sent second.
|
||||
return 'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state)
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
return "Received test message."
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
see(dev_id=device, location_name=STATE_NOT_HOME)
|
||||
handler.write_text("Setting location to not home")
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered before
|
||||
# the previous zone was exited. The enter message will be sent
|
||||
# first, then the exit message will be sent second.
|
||||
handler.write_text(
|
||||
'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state))
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
handler.write_text("Received test message.")
|
||||
|
||||
else:
|
||||
handler.write_text(
|
||||
"Received unidentified message: {}".format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Received unidentified message from Locative: %s",
|
||||
direction)
|
||||
|
||||
|
||||
def _check_data(handler, data):
|
||||
"""Check the data."""
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
handler.write_text("Latitude and longitude not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Latitude and longitude not specified.")
|
||||
return False
|
||||
|
||||
if 'device' not in data:
|
||||
handler.write_text("Device id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Device id not specified.")
|
||||
return False
|
||||
|
||||
if 'id' not in data:
|
||||
handler.write_text("Location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Location id not specified.")
|
||||
return False
|
||||
|
||||
if 'trigger' not in data:
|
||||
handler.write_text("Trigger is not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Trigger is not specified.")
|
||||
return False
|
||||
|
||||
return True
|
||||
_LOGGER.error("Received unidentified message from Locative: %s",
|
||||
direction)
|
||||
return ("Received unidentified message: {}".format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
@@ -21,10 +21,10 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# interval in minutes to exclude devices from a scan while they are home
|
||||
# Interval in minutes to exclude devices from a scan while they are home
|
||||
CONF_HOME_INTERVAL = "home_interval"
|
||||
|
||||
REQUIREMENTS = ['python-nmap==0.6.0']
|
||||
REQUIREMENTS = ['python-nmap==0.6.1']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
|
||||
@@ -186,7 +186,7 @@ def setup_scanner(hass, config, see):
|
||||
def _parse_see_args(topic, data):
|
||||
"""Parse the OwnTracks location parameters, into the format see expects."""
|
||||
parts = topic.split('/')
|
||||
dev_id = '{}_{}'.format(parts[1], parts[2])
|
||||
dev_id = slugify('{}_{}'.format(parts[1], parts[2]))
|
||||
host_name = parts[1]
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.util import Throttle
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pysnmp==4.2.5']
|
||||
REQUIREMENTS = ['pysnmp==4.3.2']
|
||||
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_BASEOID = "baseoid"
|
||||
@@ -72,7 +72,7 @@ class SnmpScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the WAP is up to date.
|
||||
"""Ensure the information from the device is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
@@ -88,7 +88,7 @@ class SnmpScanner(object):
|
||||
return True
|
||||
|
||||
def get_snmp_data(self):
|
||||
"""Fetch MAC addresses from WAP via SNMP."""
|
||||
"""Fetch MAC addresses from access point via SNMP."""
|
||||
devices = []
|
||||
|
||||
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
|
||||
@@ -97,9 +97,10 @@ class SnmpScanner(object):
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
return
|
||||
# pylint: disable=no-member
|
||||
if errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
errindex and restable[-1][int(errindex)-1] or '?')
|
||||
errindex and restable[int(errindex) - 1][0] or '?')
|
||||
return
|
||||
|
||||
for resrow in restable:
|
||||
|
||||
@@ -16,6 +16,7 @@ REQUIREMENTS = ['urllib3', 'unifi==1.2.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
CONF_SITE_ID = 'site_id'
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
@@ -32,6 +33,7 @@ def get_scanner(hass, config):
|
||||
host = this_config.get(CONF_HOST, 'localhost')
|
||||
username = this_config.get(CONF_USERNAME)
|
||||
password = this_config.get(CONF_PASSWORD)
|
||||
site_id = this_config.get(CONF_SITE_ID, 'default')
|
||||
|
||||
try:
|
||||
port = int(this_config.get(CONF_PORT, 8443))
|
||||
@@ -40,7 +42,7 @@ def get_scanner(hass, config):
|
||||
return False
|
||||
|
||||
try:
|
||||
ctrl = Controller(host, username, password, port, 'v4')
|
||||
ctrl = Controller(host, username, password, port, 'v4', site_id)
|
||||
except urllib.error.HTTPError as ex:
|
||||
_LOGGER.error('Failed to connect to unifi: %s', ex)
|
||||
return False
|
||||
|
||||
@@ -9,100 +9,31 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_HOMEASSISTANT_START,
|
||||
EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
DOMAIN = "discovery"
|
||||
REQUIREMENTS = ['netdisco==0.6.7']
|
||||
REQUIREMENTS = ['netdisco==0.7.1']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
LOAD_PLATFORM = 'load_platform'
|
||||
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_CAST = 'google_cast'
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
SERVICE_SONOS = 'sonos'
|
||||
SERVICE_PLEX = 'plex_mediaserver'
|
||||
SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
|
||||
SERVICE_PANASONIC_VIERA = 'panasonic_viera'
|
||||
SERVICE_ROKU = 'roku'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_WEMO: "wemo",
|
||||
SERVICE_CAST: "media_player",
|
||||
SERVICE_HUE: "light",
|
||||
SERVICE_NETGEAR: 'device_tracker',
|
||||
SERVICE_SONOS: 'media_player',
|
||||
SERVICE_PLEX: 'media_player',
|
||||
SERVICE_SQUEEZEBOX: 'media_player',
|
||||
SERVICE_PANASONIC_VIERA: 'media_player',
|
||||
SERVICE_ROKU: 'media_player',
|
||||
SERVICE_NETGEAR: ('device_tracker', None),
|
||||
SERVICE_WEMO: ('wemo', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
'roku': ('media_player', 'roku'),
|
||||
'sonos': ('media_player', 'sonos'),
|
||||
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
||||
'directv': ('media_player', 'directv'),
|
||||
}
|
||||
|
||||
|
||||
def listen(hass, service, callback):
|
||||
"""Setup listener for discovery of specific service.
|
||||
|
||||
Service can be a string or a list/tuple.
|
||||
"""
|
||||
if isinstance(service, str):
|
||||
service = (service,)
|
||||
else:
|
||||
service = tuple(service)
|
||||
|
||||
def discovery_event_listener(event):
|
||||
"""Listen for discovery events."""
|
||||
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
|
||||
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
|
||||
|
||||
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
|
||||
|
||||
|
||||
def discover(hass, service, discovered=None, component=None, hass_config=None):
|
||||
"""Fire discovery event. Can ensure a component is loaded."""
|
||||
if component is not None:
|
||||
bootstrap.setup_component(hass, component, hass_config)
|
||||
|
||||
data = {
|
||||
ATTR_SERVICE: service
|
||||
}
|
||||
|
||||
if discovered is not None:
|
||||
data[ATTR_DISCOVERED] = discovered
|
||||
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
|
||||
|
||||
|
||||
def load_platform(hass, component, platform, info=None, hass_config=None):
|
||||
"""Helper method for generic platform loading.
|
||||
|
||||
This method allows a platform to be loaded dynamically without it being
|
||||
known at runtime (in the DISCOVERY_PLATFORMS list of the component).
|
||||
Advantages of using this method:
|
||||
- Any component & platforms combination can be dynamically added
|
||||
- A component (i.e. light) does not have to import every component
|
||||
that can dynamically add a platform (e.g. wemo, wink, insteon_hub)
|
||||
- Custom user components can take advantage of discovery/loading
|
||||
|
||||
Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
|
||||
fired to load the platform. The event will contain:
|
||||
{ ATTR_SERVICE = LOAD_PLATFORM + '.' + <<component>>
|
||||
ATTR_DISCOVERED = {LOAD_PLATFORM: <<platform>>} }
|
||||
|
||||
* dev note: This listener can be found in entity_component.py
|
||||
"""
|
||||
if info is None:
|
||||
info = {LOAD_PLATFORM: platform}
|
||||
else:
|
||||
info[LOAD_PLATFORM] = platform
|
||||
discover(hass, LOAD_PLATFORM + '.' + component, info, component,
|
||||
hass_config)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Start a discovery service."""
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -119,20 +50,18 @@ def setup(hass, config):
|
||||
with lock:
|
||||
logger.info("Found new service: %s %s", service, info)
|
||||
|
||||
component = SERVICE_HANDLERS.get(service)
|
||||
comp_plat = SERVICE_HANDLERS.get(service)
|
||||
|
||||
# We do not know how to handle this service.
|
||||
if not component:
|
||||
if not comp_plat:
|
||||
return
|
||||
|
||||
# This component cannot be setup.
|
||||
if not bootstrap.setup_component(hass, component, config):
|
||||
return
|
||||
component, platform = comp_plat
|
||||
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: service,
|
||||
ATTR_DISCOVERED: info
|
||||
})
|
||||
if platform is None:
|
||||
discover(hass, service, info, component, config)
|
||||
else:
|
||||
load_platform(hass, component, platform, info, config)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def start_discovery(event):
|
||||
|
||||
@@ -24,7 +24,8 @@ ATTR_URL = "url"
|
||||
ATTR_SUBDIR = "subdir"
|
||||
|
||||
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_URL): vol.Url,
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Required(ATTR_URL): vol.Url(),
|
||||
vol.Optional(ATTR_SUBDIR): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -8,21 +8,18 @@ import logging
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = "ecobee"
|
||||
DISCOVER_THERMOSTAT = "ecobee.thermostat"
|
||||
DISCOVER_SENSORS = "ecobee.sensor"
|
||||
NETWORK = None
|
||||
HOLD_TEMP = 'hold_temp'
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/nkgilley/python-ecobee-api/archive/'
|
||||
'4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5']
|
||||
'4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -70,23 +67,11 @@ def setup_ecobee(hass, network, config):
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(_CONFIGURING.pop('ecobee'))
|
||||
|
||||
# Ensure component is loaded
|
||||
bootstrap.setup_component(hass, 'thermostat', config)
|
||||
bootstrap.setup_component(hass, 'sensor', config)
|
||||
|
||||
hold_temp = config[DOMAIN].get(HOLD_TEMP, False)
|
||||
|
||||
# Fire thermostat discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: DISCOVER_THERMOSTAT,
|
||||
ATTR_DISCOVERED: {'hold_temp': hold_temp}
|
||||
})
|
||||
|
||||
# Fire sensor discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: DISCOVER_SENSORS,
|
||||
ATTR_DISCOVERED: {}
|
||||
})
|
||||
discovery.load_platform(hass, 'thermostat', DOMAIN,
|
||||
{'hold_temp': hold_temp}, config)
|
||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
117
homeassistant/components/enocean.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
EnOcean Component.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/EnOcean/
|
||||
"""
|
||||
|
||||
DOMAIN = "enocean"
|
||||
|
||||
REQUIREMENTS = ['enocean==0.31']
|
||||
|
||||
CONF_DEVICE = "device"
|
||||
|
||||
ENOCEAN_DONGLE = None
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the EnOcean component."""
|
||||
global ENOCEAN_DONGLE
|
||||
|
||||
serial_dev = config[DOMAIN].get(CONF_DEVICE, "/dev/ttyUSB0")
|
||||
|
||||
ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev)
|
||||
return True
|
||||
|
||||
|
||||
class EnOceanDongle:
|
||||
"""Representation of an EnOcean dongle."""
|
||||
|
||||
def __init__(self, hass, ser):
|
||||
"""Initialize the EnOcean dongle."""
|
||||
from enocean.communicators.serialcommunicator import SerialCommunicator
|
||||
self.__communicator = SerialCommunicator(port=ser,
|
||||
callback=self.callback)
|
||||
self.__communicator.start()
|
||||
self.__devices = []
|
||||
|
||||
def register_device(self, dev):
|
||||
"""Register another device."""
|
||||
self.__devices.append(dev)
|
||||
|
||||
def send_command(self, command):
|
||||
"""Send a command from the EnOcean dongle."""
|
||||
self.__communicator.send(command)
|
||||
|
||||
def _combine_hex(self, data): # pylint: disable=no-self-use
|
||||
"""Combine list of integer values to one big integer."""
|
||||
output = 0x00
|
||||
for i, j in enumerate(reversed(data)):
|
||||
output |= (j << i * 8)
|
||||
return output
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def callback(self, temp):
|
||||
"""Callback function for EnOcean Device.
|
||||
|
||||
This is the callback function called by
|
||||
python-enocan whenever there is an incoming
|
||||
packet.
|
||||
"""
|
||||
from enocean.protocol.packet import RadioPacket
|
||||
if isinstance(temp, RadioPacket):
|
||||
rxtype = None
|
||||
value = None
|
||||
if temp.data[6] == 0x30:
|
||||
rxtype = "wallswitch"
|
||||
value = 1
|
||||
elif temp.data[6] == 0x20:
|
||||
rxtype = "wallswitch"
|
||||
value = 0
|
||||
elif temp.data[4] == 0x0c:
|
||||
rxtype = "power"
|
||||
value = temp.data[3] + (temp.data[2] << 8)
|
||||
elif temp.data[2] == 0x60:
|
||||
rxtype = "switch_status"
|
||||
if temp.data[3] == 0xe4:
|
||||
value = 1
|
||||
elif temp.data[3] == 0x80:
|
||||
value = 0
|
||||
elif temp.data[0] == 0xa5 and temp.data[1] == 0x02:
|
||||
rxtype = "dimmerstatus"
|
||||
value = temp.data[2]
|
||||
for device in self.__devices:
|
||||
if rxtype == "wallswitch" and device.stype == "listener":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value, temp.data[1])
|
||||
if rxtype == "power" and device.stype == "powersensor":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
if rxtype == "power" and device.stype == "switch":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
if value > 10:
|
||||
device.value_changed(1)
|
||||
if rxtype == "switch_status" and device.stype == "switch":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
if rxtype == "dimmerstatus" and device.stype == "dimmer":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class EnOceanDevice():
|
||||
"""Parent class for all devices associated with the EnOcean component."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the device."""
|
||||
ENOCEAN_DONGLE.register_device(self)
|
||||
self.stype = ""
|
||||
self.sensorid = [0x00, 0x00, 0x00, 0x00]
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def send_command(self, data, optional, packet_type):
|
||||
"""Send a command via the EnOcean dongle."""
|
||||
from enocean.protocol.packet import Packet
|
||||
packet = Packet(packet_type, data=data, optional=optional)
|
||||
ENOCEAN_DONGLE.send_command(packet)
|
||||
210
homeassistant/components/envisalink.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Support for Envisalink devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/envisalink/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['pyenvisalink==1.0', 'pydispatcher==2.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'envisalink'
|
||||
|
||||
EVL_CONTROLLER = None
|
||||
|
||||
CONF_EVL_HOST = 'host'
|
||||
CONF_EVL_PORT = 'port'
|
||||
CONF_PANEL_TYPE = 'panel_type'
|
||||
CONF_EVL_VERSION = 'evl_version'
|
||||
CONF_CODE = 'code'
|
||||
CONF_USERNAME = 'user_name'
|
||||
CONF_PASS = 'password'
|
||||
CONF_EVL_KEEPALIVE = 'keepalive_interval'
|
||||
CONF_ZONEDUMP_INTERVAL = 'zonedump_interval'
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_PARTITIONS = 'partitions'
|
||||
|
||||
CONF_ZONENAME = 'name'
|
||||
CONF_ZONETYPE = 'type'
|
||||
CONF_PARTITIONNAME = 'name'
|
||||
|
||||
DEFAULT_PORT = 4025
|
||||
DEFAULT_EVL_VERSION = 3
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_ZONEDUMP_INTERVAL = 30
|
||||
DEFAULT_ZONETYPE = 'opening'
|
||||
|
||||
SIGNAL_ZONE_UPDATE = 'zones_updated'
|
||||
SIGNAL_PARTITION_UPDATE = 'partition_updated'
|
||||
SIGNAL_KEYPAD_UPDATE = 'keypad_updated'
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONENAME): cv.string,
|
||||
vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string})
|
||||
|
||||
PARTITION_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PARTITIONNAME): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_EVL_HOST): cv.string,
|
||||
vol.Required(CONF_PANEL_TYPE):
|
||||
vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASS): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
|
||||
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=3, max=4)),
|
||||
vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||
vol.Optional(CONF_ZONEDUMP_INTERVAL,
|
||||
default=DEFAULT_ZONEDUMP_INTERVAL):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, too-many-function-args, too-many-locals
|
||||
# pylint: disable=too-many-return-statements
|
||||
def setup(hass, base_config):
|
||||
"""Common setup for Envisalink devices."""
|
||||
from pyenvisalink import EnvisalinkAlarmPanel
|
||||
from pydispatch import dispatcher
|
||||
|
||||
global EVL_CONTROLLER
|
||||
|
||||
config = base_config.get(DOMAIN)
|
||||
|
||||
_host = config.get(CONF_EVL_HOST)
|
||||
_port = config.get(CONF_EVL_PORT)
|
||||
_code = config.get(CONF_CODE)
|
||||
_panel_type = config.get(CONF_PANEL_TYPE)
|
||||
_version = config.get(CONF_EVL_VERSION)
|
||||
_user = config.get(CONF_USERNAME)
|
||||
_pass = config.get(CONF_PASS)
|
||||
_keep_alive = config.get(CONF_EVL_KEEPALIVE)
|
||||
_zone_dump = config.get(CONF_ZONEDUMP_INTERVAL)
|
||||
_zones = config.get(CONF_ZONES)
|
||||
_partitions = config.get(CONF_PARTITIONS)
|
||||
_connect_status = {}
|
||||
EVL_CONTROLLER = EnvisalinkAlarmPanel(_host,
|
||||
_port,
|
||||
_panel_type,
|
||||
_version,
|
||||
_user,
|
||||
_pass,
|
||||
_zone_dump,
|
||||
_keep_alive)
|
||||
|
||||
def login_fail_callback(data):
|
||||
"""Callback for when the evl rejects our login."""
|
||||
_LOGGER.error("The envisalink rejected your credentials.")
|
||||
_connect_status['fail'] = 1
|
||||
|
||||
def connection_fail_callback(data):
|
||||
"""Network failure callback."""
|
||||
_LOGGER.error("Could not establish a connection with the envisalink.")
|
||||
_connect_status['fail'] = 1
|
||||
|
||||
def connection_success_callback(data):
|
||||
"""Callback for a successful connection."""
|
||||
_LOGGER.info("Established a connection with the envisalink.")
|
||||
_connect_status['success'] = 1
|
||||
|
||||
def zones_updated_callback(data):
|
||||
"""Handle zone timer updates."""
|
||||
_LOGGER.info("Envisalink sent a zone update event. Updating zones...")
|
||||
dispatcher.send(signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=None,
|
||||
zone=data)
|
||||
|
||||
def alarm_data_updated_callback(data):
|
||||
"""Handle non-alarm based info updates."""
|
||||
_LOGGER.info("Envisalink sent new alarm info. Updating alarms...")
|
||||
dispatcher.send(signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
|
||||
def partition_updated_callback(data):
|
||||
"""Handle partition changes thrown by evl (including alarms)."""
|
||||
_LOGGER.info("The envisalink sent a partition update event.")
|
||||
dispatcher.send(signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
|
||||
def stop_envisalink(event):
|
||||
"""Shutdown envisalink connection and thread on exit."""
|
||||
_LOGGER.info("Shutting down envisalink.")
|
||||
EVL_CONTROLLER.stop()
|
||||
|
||||
def start_envisalink(event):
|
||||
"""Startup process for the Envisalink."""
|
||||
EVL_CONTROLLER.start()
|
||||
for _ in range(10):
|
||||
if 'success' in _connect_status:
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
|
||||
return True
|
||||
elif 'fail' in _connect_status:
|
||||
return False
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
_LOGGER.error("Timeout occurred while establishing evl connection.")
|
||||
return False
|
||||
|
||||
EVL_CONTROLLER.callback_zone_timer_dump = zones_updated_callback
|
||||
EVL_CONTROLLER.callback_zone_state_change = zones_updated_callback
|
||||
EVL_CONTROLLER.callback_partition_state_change = partition_updated_callback
|
||||
EVL_CONTROLLER.callback_keypad_update = alarm_data_updated_callback
|
||||
EVL_CONTROLLER.callback_login_failure = login_fail_callback
|
||||
EVL_CONTROLLER.callback_login_timeout = connection_fail_callback
|
||||
EVL_CONTROLLER.callback_login_success = connection_success_callback
|
||||
|
||||
_result = start_envisalink(None)
|
||||
if not _result:
|
||||
return False
|
||||
|
||||
# Load sub-components for Envisalink
|
||||
if _partitions:
|
||||
load_platform(hass, 'alarm_control_panel', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
load_platform(hass, 'sensor', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
if _zones:
|
||||
load_platform(hass, 'binary_sensor', 'envisalink',
|
||||
{'zones': _zones}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkDevice(Entity):
|
||||
"""Representation of an Envisalink device."""
|
||||
|
||||
def __init__(self, name, info, controller):
|
||||
"""Initialize the device."""
|
||||
self._controller = controller
|
||||
self._info = info
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
@@ -6,7 +6,11 @@ https://home-assistant.io/components/feedreader/
|
||||
"""
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
from os.path import exists
|
||||
from threading import Lock
|
||||
import pickle
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
@@ -27,14 +31,15 @@ MAX_ENTRIES = 20
|
||||
class FeedManager(object):
|
||||
"""Abstraction over feedparser module."""
|
||||
|
||||
def __init__(self, url, hass):
|
||||
def __init__(self, url, hass, storage):
|
||||
"""Initialize the FeedManager object, poll every hour."""
|
||||
self._url = url
|
||||
self._feed = None
|
||||
self._hass = hass
|
||||
self._firstrun = True
|
||||
# Initialize last entry timestamp as epoch time
|
||||
self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple()
|
||||
self._storage = storage
|
||||
self._last_entry_timestamp = None
|
||||
self._has_published_parsed = False
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||
lambda _: self._update())
|
||||
track_utc_time_change(hass, lambda now: self._update(),
|
||||
@@ -42,7 +47,7 @@ class FeedManager(object):
|
||||
|
||||
def _log_no_entries(self):
|
||||
"""Send no entries log at debug level."""
|
||||
_LOGGER.debug('No new entries in feed "%s"', self._url)
|
||||
_LOGGER.debug('No new entries to be published in feed "%s"', self._url)
|
||||
|
||||
def _update(self):
|
||||
"""Update the feed and publish new entries to the event bus."""
|
||||
@@ -65,10 +70,13 @@ class FeedManager(object):
|
||||
len(self._feed.entries),
|
||||
self._url)
|
||||
if len(self._feed.entries) > MAX_ENTRIES:
|
||||
_LOGGER.debug('Publishing only the first %s entries '
|
||||
_LOGGER.debug('Processing only the first %s entries '
|
||||
'in feed "%s"', MAX_ENTRIES, self._url)
|
||||
self._feed.entries = self._feed.entries[0:MAX_ENTRIES]
|
||||
self._publish_new_entries()
|
||||
if self._has_published_parsed:
|
||||
self._storage.put_timestamp(self._url,
|
||||
self._last_entry_timestamp)
|
||||
else:
|
||||
self._log_no_entries()
|
||||
_LOGGER.info('Fetch from feed "%s" completed', self._url)
|
||||
@@ -79,9 +87,11 @@ class FeedManager(object):
|
||||
# let's make use of it to publish only new available
|
||||
# entries since the last run
|
||||
if 'published_parsed' in entry.keys():
|
||||
self._has_published_parsed = True
|
||||
self._last_entry_timestamp = max(entry.published_parsed,
|
||||
self._last_entry_timestamp)
|
||||
else:
|
||||
self._has_published_parsed = False
|
||||
_LOGGER.debug('No `published_parsed` info available '
|
||||
'for entry "%s"', entry.title)
|
||||
entry.update({'feed_url': self._url})
|
||||
@@ -90,6 +100,13 @@ class FeedManager(object):
|
||||
def _publish_new_entries(self):
|
||||
"""Publish new entries to the event bus."""
|
||||
new_entries = False
|
||||
self._last_entry_timestamp = self._storage.get_timestamp(self._url)
|
||||
if self._last_entry_timestamp:
|
||||
self._firstrun = False
|
||||
else:
|
||||
# Set last entry timestamp as epoch time if not available
|
||||
self._last_entry_timestamp = \
|
||||
datetime.utcfromtimestamp(0).timetuple()
|
||||
for entry in self._feed.entries:
|
||||
if self._firstrun or (
|
||||
'published_parsed' in entry.keys() and
|
||||
@@ -103,8 +120,55 @@ class FeedManager(object):
|
||||
self._firstrun = False
|
||||
|
||||
|
||||
class StoredData(object):
|
||||
"""Abstraction over pickle data storage."""
|
||||
|
||||
def __init__(self, data_file):
|
||||
"""Initialize pickle data storage."""
|
||||
self._data_file = data_file
|
||||
self._lock = Lock()
|
||||
self._cache_outdated = True
|
||||
self._data = {}
|
||||
self._fetch_data()
|
||||
|
||||
def _fetch_data(self):
|
||||
"""Fetch data stored into pickle file."""
|
||||
if self._cache_outdated and exists(self._data_file):
|
||||
try:
|
||||
_LOGGER.debug('Fetching data from file %s', self._data_file)
|
||||
with self._lock, open(self._data_file, 'rb') as myfile:
|
||||
self._data = pickle.load(myfile) or {}
|
||||
self._cache_outdated = False
|
||||
# pylint: disable=bare-except
|
||||
except:
|
||||
_LOGGER.error('Error loading data from pickled file %s',
|
||||
self._data_file)
|
||||
|
||||
def get_timestamp(self, url):
|
||||
"""Return stored timestamp for given url."""
|
||||
self._fetch_data()
|
||||
return self._data.get(url)
|
||||
|
||||
def put_timestamp(self, url, timestamp):
|
||||
"""Update timestamp for given url."""
|
||||
self._fetch_data()
|
||||
with self._lock, open(self._data_file, 'wb') as myfile:
|
||||
self._data.update({url: timestamp})
|
||||
_LOGGER.debug('Overwriting feed "%s" timestamp in storage file %s',
|
||||
url, self._data_file)
|
||||
try:
|
||||
pickle.dump(self._data, myfile)
|
||||
# pylint: disable=bare-except
|
||||
except:
|
||||
_LOGGER.error('Error saving pickled data to %s',
|
||||
self._data_file)
|
||||
self._cache_outdated = True
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the feedreader component."""
|
||||
urls = config.get(DOMAIN)['urls']
|
||||
feeds = [FeedManager(url, hass) for url in urls]
|
||||
data_file = hass.config.path("{}.pickle".format(DOMAIN))
|
||||
storage = StoredData(data_file)
|
||||
feeds = [FeedManager(url, hass, storage) for url in urls]
|
||||
return len(feeds) > 0
|
||||
|
||||
99
homeassistant/components/foursquare.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Allows utilizing the Foursquare (Swarm) API.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/foursquare/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = "foursquare"
|
||||
|
||||
SERVICE_CHECKIN = "checkin"
|
||||
|
||||
EVENT_PUSH = "foursquare.push"
|
||||
EVENT_CHECKIN = "foursquare.checkin"
|
||||
|
||||
CHECKIN_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required("venueId"): cv.string,
|
||||
vol.Optional("eventId"): cv.string,
|
||||
vol.Optional("shout"): cv.string,
|
||||
vol.Optional("mentions"): cv.string,
|
||||
vol.Optional("broadcast"): cv.string,
|
||||
vol.Optional("ll"): cv.string,
|
||||
vol.Optional("llAcc"): cv.string,
|
||||
vol.Optional("alt"): cv.string,
|
||||
vol.Optional("altAcc"): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["http"]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Foursquare component."""
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), "services.yaml"))
|
||||
|
||||
config = config[DOMAIN]
|
||||
|
||||
def checkin_user(call):
|
||||
"""Check a user in on Swarm."""
|
||||
url = ("https://api.foursquare.com/v2/checkins/add"
|
||||
"?oauth_token={}"
|
||||
"&v=20160802"
|
||||
"&m=swarm").format(config["access_token"])
|
||||
response = requests.post(url, data=call.data, timeout=10)
|
||||
|
||||
if response.status_code not in (200, 201):
|
||||
_LOGGER.exception(
|
||||
"Error checking in user. Response %d: %s:",
|
||||
response.status_code, response.reason)
|
||||
|
||||
hass.bus.fire(EVENT_CHECKIN, response.text)
|
||||
|
||||
# Register our service with Home Assistant.
|
||||
hass.services.register(DOMAIN, "checkin", checkin_user,
|
||||
descriptions[DOMAIN][SERVICE_CHECKIN],
|
||||
schema=CHECKIN_SERVICE_SCHEMA)
|
||||
|
||||
hass.wsgi.register_view(FoursquarePushReceiver(hass,
|
||||
config["push_secret"]))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class FoursquarePushReceiver(HomeAssistantView):
|
||||
"""Handle pushes from the Foursquare API."""
|
||||
|
||||
requires_auth = False
|
||||
url = "/api/foursquare"
|
||||
name = "foursquare"
|
||||
|
||||
def __init__(self, hass, push_secret):
|
||||
"""Initialize the OAuth callback view."""
|
||||
super().__init__(hass)
|
||||
self.push_secret = push_secret
|
||||
|
||||
def post(self, request):
|
||||
"""Accept the POST from Foursquare."""
|
||||
raw_data = request.form
|
||||
_LOGGER.debug("Received Foursquare push: %s", raw_data)
|
||||
if self.push_secret != raw_data["secret"]:
|
||||
_LOGGER.error("Received Foursquare push with invalid"
|
||||
"push secret! Data: %s", raw_data)
|
||||
return
|
||||
parsed_payload = {
|
||||
key: json.loads(val) for key, val in raw_data.items()
|
||||
if key != "secret"
|
||||
}
|
||||
self.hass.bus.fire(EVENT_PUSH, parsed_payload)
|
||||
@@ -1,121 +1,201 @@
|
||||
"""Handle the frontend for Home Assistant."""
|
||||
import re
|
||||
import os
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
from . import version, mdi_version
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import URL_ROOT, HTTP_OK
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.components import api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from .version import FINGERPRINTS
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
|
||||
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||
PANELS = {}
|
||||
|
||||
INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
|
||||
|
||||
# To keep track we don't register a component twice (gives a warning)
|
||||
_REGISTERED_COMPONENTS = set()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FRONTEND_URLS = [
|
||||
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent', '/devInfo', '/devTemplate',
|
||||
re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)'),
|
||||
]
|
||||
|
||||
URL_API_BOOTSTRAP = "/api/bootstrap"
|
||||
def register_built_in_panel(hass, component_name, sidebar_title=None,
|
||||
sidebar_icon=None, url_path=None, config=None):
|
||||
"""Register a built-in panel."""
|
||||
# pylint: disable=too-many-arguments
|
||||
path = 'panels/ha-panel-{}.html'.format(component_name)
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
if hass.wsgi.development:
|
||||
url = ('/static/home-assistant-polymer/panels/'
|
||||
'{0}/ha-panel-{0}.html'.format(component_name))
|
||||
else:
|
||||
url = None # use default url generate mechanism
|
||||
|
||||
register_panel(hass, component_name, os.path.join(STATIC_PATH, path),
|
||||
FINGERPRINTS[path], sidebar_title, sidebar_icon, url_path,
|
||||
url, config)
|
||||
|
||||
|
||||
def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||
sidebar_icon=None, url_path=None, url=None, config=None):
|
||||
"""Register a panel for the frontend.
|
||||
|
||||
component_name: name of the web component
|
||||
path: path to the HTML of the web component
|
||||
md5: the md5 hash of the web component (for versioning, optional)
|
||||
sidebar_title: title to show in the sidebar (optional)
|
||||
sidebar_icon: icon to show next to title in sidebar (optional)
|
||||
url_path: name to use in the url (defaults to component_name)
|
||||
url: for the web component (for dev environment, optional)
|
||||
config: config to be passed into the web component
|
||||
|
||||
Warning: this API will probably change. Use at own risk.
|
||||
"""
|
||||
# pylint: disable=too-many-arguments
|
||||
if url_path is None:
|
||||
url_path = component_name
|
||||
|
||||
if url_path in PANELS:
|
||||
_LOGGER.warning('Overwriting component %s', url_path)
|
||||
if not os.path.isfile(path):
|
||||
_LOGGER.error('Panel %s component does not exist: %s',
|
||||
component_name, path)
|
||||
return
|
||||
|
||||
if md5 is None:
|
||||
with open(path) as fil:
|
||||
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
|
||||
|
||||
data = {
|
||||
'url_path': url_path,
|
||||
'component_name': component_name,
|
||||
}
|
||||
|
||||
if sidebar_title:
|
||||
data['title'] = sidebar_title
|
||||
if sidebar_icon:
|
||||
data['icon'] = sidebar_icon
|
||||
if config is not None:
|
||||
data['config'] = config
|
||||
|
||||
if url is not None:
|
||||
data['url'] = url
|
||||
else:
|
||||
url = URL_PANEL_COMPONENT.format(component_name)
|
||||
|
||||
if url not in _REGISTERED_COMPONENTS:
|
||||
hass.wsgi.register_static_path(url, path)
|
||||
_REGISTERED_COMPONENTS.add(url)
|
||||
|
||||
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
|
||||
data['url'] = fprinted_url
|
||||
|
||||
PANELS[url_path] = data
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup serving the frontend."""
|
||||
for url in FRONTEND_URLS:
|
||||
hass.http.register_path('GET', url, _handle_get_root, False)
|
||||
hass.wsgi.register_view(BootstrapView)
|
||||
|
||||
hass.http.register_path('GET', '/service_worker.js',
|
||||
_handle_get_service_worker, False)
|
||||
|
||||
# Bootstrap API
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
|
||||
|
||||
# Static files
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_static, False)
|
||||
hass.http.register_path(
|
||||
'HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_static, False)
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/local/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_local, False)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_bootstrap(handler, path_match, data):
|
||||
"""Return all data needed to bootstrap Home Assistant."""
|
||||
hass = handler.server.hass
|
||||
|
||||
handler.write_json({
|
||||
'config': hass.config.as_dict(),
|
||||
'states': hass.states.all(),
|
||||
'events': api.events_json(hass),
|
||||
'services': api.services_json(hass),
|
||||
})
|
||||
|
||||
|
||||
def _handle_get_root(handler, path_match, data):
|
||||
"""Render the frontend."""
|
||||
if handler.server.development:
|
||||
app_url = "home-assistant-polymer/src/home-assistant.html"
|
||||
else:
|
||||
app_url = "frontend-{}.html".format(version.VERSION)
|
||||
|
||||
# auto login if no password was set, else check api_password param
|
||||
auth = ('no_password_set' if handler.server.api_password is None
|
||||
else data.get('api_password', ''))
|
||||
|
||||
with open(INDEX_PATH) as template_file:
|
||||
template_html = template_file.read()
|
||||
|
||||
template_html = template_html.replace('{{ app_url }}', app_url)
|
||||
template_html = template_html.replace('{{ auth }}', auth)
|
||||
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.write_content(template_html.encode("UTF-8"),
|
||||
'text/html; charset=utf-8')
|
||||
|
||||
|
||||
def _handle_get_service_worker(handler, path_match, data):
|
||||
"""Return service worker for the frontend."""
|
||||
if handler.server.development:
|
||||
if hass.wsgi.development:
|
||||
sw_path = "home-assistant-polymer/build/service_worker.js"
|
||||
else:
|
||||
sw_path = "service_worker.js"
|
||||
|
||||
handler.write_file(os.path.join(os.path.dirname(__file__), 'www_static',
|
||||
sw_path))
|
||||
hass.wsgi.register_static_path("/service_worker.js",
|
||||
os.path.join(STATIC_PATH, sw_path), 0)
|
||||
hass.wsgi.register_static_path("/robots.txt",
|
||||
os.path.join(STATIC_PATH, "robots.txt"))
|
||||
hass.wsgi.register_static_path("/static", STATIC_PATH)
|
||||
hass.wsgi.register_static_path("/local", hass.config.path('www'))
|
||||
|
||||
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
||||
|
||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template'):
|
||||
register_built_in_panel(hass, panel)
|
||||
|
||||
def register_frontend_index(event):
|
||||
"""Register the frontend index urls.
|
||||
|
||||
Done when Home Assistant is started so that all panels are known.
|
||||
"""
|
||||
hass.wsgi.register_view(IndexView(
|
||||
hass, ['/{}'.format(name) for name in PANELS]))
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_static(handler, path_match, data):
|
||||
"""Return a static file for the frontend."""
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
class BootstrapView(HomeAssistantView):
|
||||
"""View to bootstrap frontend with all needed data."""
|
||||
|
||||
# Strip md5 hash out
|
||||
fingerprinted = _FINGERPRINT.match(req_file)
|
||||
if fingerprinted:
|
||||
req_file = "{}.{}".format(*fingerprinted.groups())
|
||||
url = "/api/bootstrap"
|
||||
name = "api:bootstrap"
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
|
||||
|
||||
handler.write_file(path)
|
||||
def get(self, request):
|
||||
"""Return all data needed to bootstrap Home Assistant."""
|
||||
return self.json({
|
||||
'config': self.hass.config.as_dict(),
|
||||
'states': self.hass.states.all(),
|
||||
'events': api.events_json(self.hass),
|
||||
'services': api.services_json(self.hass),
|
||||
'panels': PANELS,
|
||||
})
|
||||
|
||||
|
||||
def _handle_get_local(handler, path_match, data):
|
||||
"""Return a static file from the hass.config.path/www for the frontend."""
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
class IndexView(HomeAssistantView):
|
||||
"""Serve the frontend."""
|
||||
|
||||
path = handler.server.hass.config.path('www', req_file)
|
||||
url = '/'
|
||||
name = "frontend:index"
|
||||
requires_auth = False
|
||||
extra_urls = ['/states', '/states/<entity:entity_id>']
|
||||
|
||||
handler.write_file(path)
|
||||
def __init__(self, hass, extra_urls):
|
||||
"""Initialize the frontend view."""
|
||||
super().__init__(hass)
|
||||
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
|
||||
self.extra_urls = self.extra_urls + extra_urls
|
||||
self.templates = Environment(
|
||||
loader=FileSystemLoader(
|
||||
os.path.join(os.path.dirname(__file__), 'templates/')
|
||||
)
|
||||
)
|
||||
|
||||
def get(self, request, entity_id=None):
|
||||
"""Serve the index view."""
|
||||
if self.hass.wsgi.development:
|
||||
core_url = '/static/home-assistant-polymer/build/core.js'
|
||||
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
||||
else:
|
||||
core_url = '/static/core-{}.js'.format(
|
||||
FINGERPRINTS['core.js'])
|
||||
ui_url = '/static/frontend-{}.html'.format(
|
||||
FINGERPRINTS['frontend.html'])
|
||||
|
||||
if request.path == '/':
|
||||
panel = 'states'
|
||||
else:
|
||||
panel = request.path.split('/')[1]
|
||||
|
||||
panel_url = PANELS[panel]['url'] if panel != 'states' else ''
|
||||
|
||||
# auto login if no password was set
|
||||
no_auth = 'false' if self.hass.config.api.api_password else 'true'
|
||||
|
||||
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
||||
template = self.templates.get_template('index.html')
|
||||
|
||||
# pylint is wrong
|
||||
# pylint: disable=no-member
|
||||
resp = template.render(
|
||||
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
||||
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
||||
panel_url=panel_url, panels=PANELS)
|
||||
|
||||
return self.Response(resp, mimetype='text/html')
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
|
||||
VERSION = "1baebe8155deb447230866d7ae854bd9"
|
||||
@@ -5,14 +5,29 @@
|
||||
<title>Home Assistant</title>
|
||||
|
||||
<link rel='manifest' href='/static/manifest.json'>
|
||||
<link rel='icon' href='/static/favicon.ico'>
|
||||
<link rel='icon' href='/static/icons/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/favicon-apple-180x180.png'>
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
{% for panel in panels.values() -%}
|
||||
<link rel='prefetch' href='{{ panel.url }}'>
|
||||
{% endfor -%}
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
|
||||
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
|
||||
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
|
||||
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
|
||||
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='#03a9f4'>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
#ha-init-skeleton {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
@@ -28,7 +43,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin-bottom: 97px;
|
||||
margin-bottom: 83px;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 0pt;
|
||||
transition: font-size 2s;
|
||||
@@ -36,6 +51,7 @@
|
||||
|
||||
#ha-init-skeleton paper-spinner {
|
||||
height: 28px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton a {
|
||||
@@ -58,29 +74,37 @@
|
||||
document
|
||||
.getElementById('ha-init-skeleton')
|
||||
.classList.add('error');
|
||||
}
|
||||
};
|
||||
window.noAuth = {{ no_auth }};
|
||||
window.Polymer = {lazyRegister: true, useNativeCSSProperties: true, dom: 'shady'};
|
||||
</script>
|
||||
<link rel='import' href='/static/{{ app_url }}' onerror='initError()' async>
|
||||
</head>
|
||||
<body fullbleed>
|
||||
<body>
|
||||
<div id='ha-init-skeleton'>
|
||||
<img src='/static/favicon-192x192.png' height='192'>
|
||||
<img src='/static/icons/favicon-192x192.png' height='192'>
|
||||
<paper-spinner active></paper-spinner>
|
||||
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
|
||||
</div>
|
||||
<home-assistant icons='{{ icons }}'></home-assistant>
|
||||
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
|
||||
<script src='{{ core_url }}'></script>
|
||||
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
|
||||
{% if panel_url -%}
|
||||
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
|
||||
{% endif -%}
|
||||
<link rel='import' href='{{ icons_url }}' async>
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
'registerElement' in document &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
var script = document.createElement('script')
|
||||
script.async = true
|
||||
script.onerror = initError;
|
||||
script.src = '/static/webcomponents-lite.min.js'
|
||||
document.head.appendChild(script)
|
||||
var e = document.createElement('script');
|
||||
e.async = true;
|
||||
e.onerror = initError;
|
||||
e.src = '/static/webcomponents-lite.min.js';
|
||||
document.head.appendChild(e);
|
||||
}
|
||||
</script>
|
||||
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,2 +1,16 @@
|
||||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
||||
VERSION = "0a226e905af198b2dabf1ce154844568"
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "457d5acd123e7dc38947c07984b3a5e8",
|
||||
"frontend.html": "829ee7cb591b8a63d7f22948a7aeb07a",
|
||||
"mdi.html": "b399b5d3798f5b68b0a4fbaae3432d48",
|
||||
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
|
||||
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
||||
"panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa",
|
||||
"panels/ha-panel-dev-state.html": "4608326978256644c42b13940c028e0a",
|
||||
"panels/ha-panel-dev-template.html": "0a099d4589636ed3038a3e9f020468a7",
|
||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
||||
"panels/ha-panel-map.html": "af7d04aff7dd5479c5a0016bc8d4dd7d"
|
||||
}
|
||||
|
||||
4
homeassistant/components/frontend/www_static/core.js
Normal file
BIN
homeassistant/components/frontend/www_static/core.js.gz
Normal file
BIN
homeassistant/components/frontend/www_static/frontend.html.gz
Normal file
|
After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |