mirror of
https://github.com/home-assistant/core.git
synced 2026-01-20 22:46:58 +01:00
Compare commits
994 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29b6782b42 | ||
|
|
d2df485bea | ||
|
|
77b141a355 | ||
|
|
1571b33e4a | ||
|
|
e9fa1f1f83 | ||
|
|
724d5bfe9d | ||
|
|
938c9888a6 | ||
|
|
b91e4cfb4a | ||
|
|
7dd51034cd | ||
|
|
2f60ff224f | ||
|
|
fc3a37cba2 | ||
|
|
223b7f85ee | ||
|
|
13b0beee31 | ||
|
|
ba530e5a16 | ||
|
|
aafd36d2ce | ||
|
|
0492f0abd0 | ||
|
|
940799d0da | ||
|
|
1ba60dc898 | ||
|
|
75d3d25969 | ||
|
|
06bd812b7b | ||
|
|
7cb57583e2 | ||
|
|
3e3f5db2a5 | ||
|
|
88fe28ea1b | ||
|
|
8857c48c17 | ||
|
|
3c582d1e3c | ||
|
|
19d12716ef | ||
|
|
57446cfb08 | ||
|
|
ff6cb2b452 | ||
|
|
a3ec7998b1 | ||
|
|
d892716bfa | ||
|
|
61e2da8827 | ||
|
|
abc039a3d0 | ||
|
|
7ba7747ce8 | ||
|
|
b04ff7207c | ||
|
|
72235c48fb | ||
|
|
3f7ff2b1d4 | ||
|
|
484b7b64d7 | ||
|
|
7241762bcc | ||
|
|
0a904acd4d | ||
|
|
76df759f4c | ||
|
|
4a2b956493 | ||
|
|
3aa34deaa2 | ||
|
|
b00cad7095 | ||
|
|
2cf061c768 | ||
|
|
e837e97c9d | ||
|
|
df8afe51f4 | ||
|
|
c8e6f89302 | ||
|
|
2e75a58372 | ||
|
|
ae2fd149a5 | ||
|
|
ee62c2cc2b | ||
|
|
4cfa14c29d | ||
|
|
4ce1a67c13 | ||
|
|
962463c1ab | ||
|
|
74f06b6862 | ||
|
|
d3d7d458e1 | ||
|
|
cca6b0c287 | ||
|
|
23e3b8d2f2 | ||
|
|
be9a2a043e | ||
|
|
cc4fa6cd38 | ||
|
|
4d15367956 | ||
|
|
175b49236c | ||
|
|
fd0afaa204 | ||
|
|
034cec7152 | ||
|
|
f464d591c9 | ||
|
|
06cb97adee | ||
|
|
cab46b91e3 | ||
|
|
0da09b85de | ||
|
|
95d9bc48ea | ||
|
|
6b962a2207 | ||
|
|
18b3d3df57 | ||
|
|
5f6977acda | ||
|
|
89f6ef9f6c | ||
|
|
d2ad0620ee | ||
|
|
aa13392983 | ||
|
|
6cbf19934f | ||
|
|
f938134069 | ||
|
|
7cdcb800a9 | ||
|
|
91fb2764cc | ||
|
|
82c5e2cf3c | ||
|
|
bb8981b611 | ||
|
|
b350f22a77 | ||
|
|
40da28a0c7 | ||
|
|
3bdb50510a | ||
|
|
7478c36b27 | ||
|
|
b1f2c90bd0 | ||
|
|
e53785f30c | ||
|
|
1a38354ed5 | ||
|
|
02609d0ab5 | ||
|
|
ce4f5ff29c | ||
|
|
5190cc74c5 | ||
|
|
e83f8da342 | ||
|
|
ddaeeba68b | ||
|
|
75775a561b | ||
|
|
058315720f | ||
|
|
4e0c7f8a3d | ||
|
|
c705ca4288 | ||
|
|
15ad48a7a0 | ||
|
|
c9c15c4cf7 | ||
|
|
3046bfce7b | ||
|
|
d52e2019c0 | ||
|
|
af8f6bcaba | ||
|
|
cdf0e80773 | ||
|
|
b0948bef5f | ||
|
|
70a528c04b | ||
|
|
d796625098 | ||
|
|
dc44ef7356 | ||
|
|
c5c4085ad4 | ||
|
|
09b3aba51b | ||
|
|
6f3aefde64 | ||
|
|
b3a1491482 | ||
|
|
b7ff79da24 | ||
|
|
4bf4d94344 | ||
|
|
3e26af5ff1 | ||
|
|
c1270cf0bb | ||
|
|
26fc637ab5 | ||
|
|
66c5d96b43 | ||
|
|
41f908ed39 | ||
|
|
0f5487b95a | ||
|
|
23c5159f6c | ||
|
|
4840dd297a | ||
|
|
4605742bb7 | ||
|
|
f222340c8e | ||
|
|
b17df44402 | ||
|
|
895ddc8433 | ||
|
|
d867d26612 | ||
|
|
564e328698 | ||
|
|
160b811ddf | ||
|
|
2e164e519a | ||
|
|
779188ad27 | ||
|
|
3f6349d663 | ||
|
|
ac0dc10377 | ||
|
|
0a7db98b0e | ||
|
|
8f690ff077 | ||
|
|
951fa603ff | ||
|
|
c113997609 | ||
|
|
f700635445 | ||
|
|
d49fae86e4 | ||
|
|
64611ab2be | ||
|
|
27dc2f61fb | ||
|
|
cd25c8f72d | ||
|
|
9ad1d290af | ||
|
|
90ef81d8d5 | ||
|
|
02efe903ab | ||
|
|
0bb63bf3f0 | ||
|
|
e23db5d972 | ||
|
|
e311f89056 | ||
|
|
757946293e | ||
|
|
98c6e56ea4 | ||
|
|
cd0cef6403 | ||
|
|
0d2891ebcc | ||
|
|
fb6aded2e1 | ||
|
|
8b7cfc831d | ||
|
|
987be65d55 | ||
|
|
f08b77dc4c | ||
|
|
681b84e1bd | ||
|
|
4103d7463b | ||
|
|
428750eeda | ||
|
|
0ae36e1d28 | ||
|
|
d2e8721918 | ||
|
|
bbdc196127 | ||
|
|
a417156d84 | ||
|
|
3575ddb6ef | ||
|
|
ffc4822f50 | ||
|
|
a147304be9 | ||
|
|
a001780afb | ||
|
|
7a00bf8696 | ||
|
|
7eef831ff3 | ||
|
|
9fde97efed | ||
|
|
d773ad1ecb | ||
|
|
cab1100a51 | ||
|
|
3616d7a7ea | ||
|
|
d38ad57b7d | ||
|
|
b3e966665a | ||
|
|
6e69737e88 | ||
|
|
062fe79b3f | ||
|
|
af0a44d976 | ||
|
|
43613f000d | ||
|
|
dde80850a6 | ||
|
|
11a2b8888b | ||
|
|
b700ec4faa | ||
|
|
614034d196 | ||
|
|
c9d145cb13 | ||
|
|
b6a32098d1 | ||
|
|
4cf85294db | ||
|
|
6fc68e9c8a | ||
|
|
ec88733b57 | ||
|
|
7ef2075520 | ||
|
|
fbd0dbf8ee | ||
|
|
2ba237eac8 | ||
|
|
6bf4532608 | ||
|
|
2622cf2e53 | ||
|
|
a5db23afa4 | ||
|
|
2c4166b5f2 | ||
|
|
8be9aaba4f | ||
|
|
1b16d76c40 | ||
|
|
1a6539ad41 | ||
|
|
96066e94ab | ||
|
|
78e758925b | ||
|
|
19fc48f4a0 | ||
|
|
d469970e5a | ||
|
|
50a9b3a7c0 | ||
|
|
ab837f9070 | ||
|
|
6149e509c3 | ||
|
|
f3b74079e0 | ||
|
|
c580953bd8 | ||
|
|
fc3741911c | ||
|
|
2589e78e84 | ||
|
|
ced380f0cd | ||
|
|
a33f1c61e5 | ||
|
|
8cf5ca0ba8 | ||
|
|
b20d3f8b3a | ||
|
|
1f34b3586e | ||
|
|
37dadd1ae0 | ||
|
|
fac8d4b969 | ||
|
|
efcba8f1ca | ||
|
|
abc253c4c5 | ||
|
|
3d00735341 | ||
|
|
ce75c590b1 | ||
|
|
6e6c3c5cd5 | ||
|
|
5521096c02 | ||
|
|
356013118d | ||
|
|
9a9dbcfaea | ||
|
|
1c33e01b99 | ||
|
|
d09837fef6 | ||
|
|
f5e736d271 | ||
|
|
61630783f1 | ||
|
|
077797ac4f | ||
|
|
b14f7f7ed0 | ||
|
|
3d695405b7 | ||
|
|
55932b048e | ||
|
|
b19fbd8e72 | ||
|
|
635369ad65 | ||
|
|
bd8881cbe1 | ||
|
|
847e92f57a | ||
|
|
5cea8fda9f | ||
|
|
7f87df20c2 | ||
|
|
e91c8e4143 | ||
|
|
cd00ff8b56 | ||
|
|
018329b12b | ||
|
|
a955f3db08 | ||
|
|
d344defc7e | ||
|
|
2da422fd77 | ||
|
|
93a38d39ef | ||
|
|
3aad223c95 | ||
|
|
1a5d18fd66 | ||
|
|
e7e540d4bb | ||
|
|
35613d7fbf | ||
|
|
00d1cab091 | ||
|
|
26efaa91a3 | ||
|
|
3c37ecc477 | ||
|
|
274aaabd93 | ||
|
|
c8bfd27182 | ||
|
|
08ab7dba2c | ||
|
|
54cc35d729 | ||
|
|
031e7a4013 | ||
|
|
41165695f0 | ||
|
|
9c33af60f2 | ||
|
|
7c1241c1f8 | ||
|
|
9caa4752a4 | ||
|
|
cb2e75befd | ||
|
|
d54e10e54a | ||
|
|
95748a6880 | ||
|
|
10a41a22dc | ||
|
|
ac0b6ca50c | ||
|
|
a0f6f3ac22 | ||
|
|
d9aff0c76d | ||
|
|
5005b20122 | ||
|
|
b3ef2bd2d9 | ||
|
|
e29a2fa45a | ||
|
|
395743005a | ||
|
|
8de56bc8e2 | ||
|
|
79b6269aa2 | ||
|
|
525b206e1b | ||
|
|
2f4e40db27 | ||
|
|
38c9f7a37a | ||
|
|
c725f7883a | ||
|
|
10f79ab45d | ||
|
|
455593017d | ||
|
|
d6b19aae48 | ||
|
|
6519333e1d | ||
|
|
41919e7339 | ||
|
|
1c10f218de | ||
|
|
96710ad410 | ||
|
|
c95c3d9198 | ||
|
|
5719743ec7 | ||
|
|
e2e8d4276f | ||
|
|
3f03fefd35 | ||
|
|
2dab815f90 | ||
|
|
6a4b63f807 | ||
|
|
8eef978241 | ||
|
|
1789a08d21 | ||
|
|
de4dab74b1 | ||
|
|
16b1529d14 | ||
|
|
0b8e097705 | ||
|
|
b21be63220 | ||
|
|
e6a8746dba | ||
|
|
1974eda51d | ||
|
|
bd475f5db1 | ||
|
|
fce8815ab4 | ||
|
|
90e17fc77f | ||
|
|
6418634f3a | ||
|
|
a230d00ed0 | ||
|
|
5fdbe5fd9a | ||
|
|
283d621e90 | ||
|
|
2d0004f46a | ||
|
|
6a08f14120 | ||
|
|
97e867052d | ||
|
|
2651021461 | ||
|
|
4b253d17ba | ||
|
|
b7722ec452 | ||
|
|
fd6086a5d6 | ||
|
|
3e35bc06fc | ||
|
|
f76dee8a05 | ||
|
|
56ac4281c7 | ||
|
|
b8e149fe7d | ||
|
|
4a8f55e630 | ||
|
|
de61bcb80e | ||
|
|
4cc9606bcc | ||
|
|
8ac763c6f6 | ||
|
|
6a75b524cb | ||
|
|
c1d057407b | ||
|
|
c396dbb570 | ||
|
|
0631f5c59d | ||
|
|
10f9c049bb | ||
|
|
d0bcec12b9 | ||
|
|
9fc62c1851 | ||
|
|
23d88cd4ad | ||
|
|
86f433067c | ||
|
|
106c53abf1 | ||
|
|
75232c43ce | ||
|
|
b56369855a | ||
|
|
01a743c7d4 | ||
|
|
28f4283b40 | ||
|
|
a41b66bb94 | ||
|
|
375faa9c91 | ||
|
|
ef132e4583 | ||
|
|
05cbe54db3 | ||
|
|
ae7697b900 | ||
|
|
a8f7bc2324 | ||
|
|
9db0987e53 | ||
|
|
515307b404 | ||
|
|
03e7ac2a0e | ||
|
|
c5cdf6d7cf | ||
|
|
a3abd8bb08 | ||
|
|
5a7e380396 | ||
|
|
09ef2e1b8c | ||
|
|
80d2f35cc5 | ||
|
|
ad57f27989 | ||
|
|
a6720f54b3 | ||
|
|
aaf75c7e45 | ||
|
|
c5de42e7b5 | ||
|
|
d6bb6a0777 | ||
|
|
f87c7d6732 | ||
|
|
575e97a051 | ||
|
|
902077d78b | ||
|
|
c17a4fca80 | ||
|
|
241a768983 | ||
|
|
72cca0a91a | ||
|
|
dd7a7f4c75 | ||
|
|
468a8a1013 | ||
|
|
24d84dbb42 | ||
|
|
893a14e8db | ||
|
|
cbc6323438 | ||
|
|
d6db00b55a | ||
|
|
eebb736bf8 | ||
|
|
f192a15a8f | ||
|
|
9ec44fbe32 | ||
|
|
ffc06e8bcb | ||
|
|
9c7b2ce9fd | ||
|
|
1de7dcdb5f | ||
|
|
9e7886b909 | ||
|
|
4045fb6862 | ||
|
|
dda4f84150 | ||
|
|
fda88f8fda | ||
|
|
3c05c8d1db | ||
|
|
21ec435430 | ||
|
|
24893bc28d | ||
|
|
403b9cbe3e | ||
|
|
8b0d19835c | ||
|
|
c31522eea9 | ||
|
|
ee33aa73e1 | ||
|
|
47498e4aa9 | ||
|
|
ef4661f1e6 | ||
|
|
b4070cfb78 | ||
|
|
c750f16275 | ||
|
|
2248c271fa | ||
|
|
87b33d5098 | ||
|
|
cf7c06d307 | ||
|
|
70d95cb6aa | ||
|
|
2c151c6db9 | ||
|
|
3fc5a60634 | ||
|
|
94d2f23cfc | ||
|
|
4bd102ddf5 | ||
|
|
232aa792f1 | ||
|
|
ce9f76a0be | ||
|
|
f3f592cdec | ||
|
|
1d2cd0811b | ||
|
|
886b581d2a | ||
|
|
88387d3123 | ||
|
|
3534c975f3 | ||
|
|
519abbbfa2 | ||
|
|
b596fa33d6 | ||
|
|
14cd27aaa7 | ||
|
|
2e175b88bc | ||
|
|
537a7789fd | ||
|
|
80d1ab78dd | ||
|
|
821238f889 | ||
|
|
33a9ec0106 | ||
|
|
a8f0f313c8 | ||
|
|
6796219f37 | ||
|
|
1dae22a465 | ||
|
|
7aba78f96e | ||
|
|
06cf07b097 | ||
|
|
271af2c608 | ||
|
|
8c6ce217e6 | ||
|
|
5410a0c8f6 | ||
|
|
1a422ecd5a | ||
|
|
6add5e387b | ||
|
|
ca070a36e3 | ||
|
|
2c3a6e7905 | ||
|
|
0a4e857901 | ||
|
|
d4b444823c | ||
|
|
2382dffbf4 | ||
|
|
33b0f4d05d | ||
|
|
5b6371ecda | ||
|
|
6f1f8ffea0 | ||
|
|
1f6f2de9c6 | ||
|
|
b13e48bd71 | ||
|
|
9a79ecf2d3 | ||
|
|
ebd475b380 | ||
|
|
cff77a175d | ||
|
|
726637b867 | ||
|
|
9117fa6eb8 | ||
|
|
6ae57b5aaf | ||
|
|
e8e2814313 | ||
|
|
a918be517d | ||
|
|
ec5d88b98e | ||
|
|
13fbefcdf8 | ||
|
|
f4c9540a1b | ||
|
|
e72f61ce73 | ||
|
|
4c056db3bb | ||
|
|
c10a86d1bf | ||
|
|
b97de5cef6 | ||
|
|
4233c0bc66 | ||
|
|
b13008201e | ||
|
|
125ad8630d | ||
|
|
5c3ad5d4c0 | ||
|
|
414aa8563d | ||
|
|
d08a181c72 | ||
|
|
80c6bf6744 | ||
|
|
b1ba792715 | ||
|
|
b99f6c1a46 | ||
|
|
1397f9e588 | ||
|
|
bb7f92330d | ||
|
|
7cbd780748 | ||
|
|
784fea2d56 | ||
|
|
97f0425252 | ||
|
|
11120a8743 | ||
|
|
8009542b3e | ||
|
|
32cfa6998c | ||
|
|
d10a5cf5e9 | ||
|
|
4089a7a0d3 | ||
|
|
5a926913d3 | ||
|
|
6c91831baa | ||
|
|
069dafa3a5 | ||
|
|
4abc5c97cd | ||
|
|
bb3dd47088 | ||
|
|
f6c53896e3 | ||
|
|
ad2e2d916b | ||
|
|
8406f81811 | ||
|
|
bcdfc555e0 | ||
|
|
3b89102338 | ||
|
|
60dd2d441d | ||
|
|
5830da63b1 | ||
|
|
50561ffe97 | ||
|
|
74e8446556 | ||
|
|
d94db5388c | ||
|
|
f66aeb2e73 | ||
|
|
54b82ecd91 | ||
|
|
2fa98167c2 | ||
|
|
57725136c0 | ||
|
|
40dbeb0b60 | ||
|
|
ef92940ffb | ||
|
|
e7865c1d67 | ||
|
|
02e634c6a2 | ||
|
|
0f937cad74 | ||
|
|
81dd1515ae | ||
|
|
a65d0f0549 | ||
|
|
881c82c2df | ||
|
|
df94c909f7 | ||
|
|
a0ed469aa2 | ||
|
|
afa4fc4ef5 | ||
|
|
df450c3d1f | ||
|
|
dc5d652d31 | ||
|
|
e4fe19fff0 | ||
|
|
f6f3f54228 | ||
|
|
6df67d2852 | ||
|
|
2411d1f2c8 | ||
|
|
9f6a1c75fa | ||
|
|
1842e5909e | ||
|
|
6f31aacb90 | ||
|
|
e541b9ba77 | ||
|
|
53484e46a3 | ||
|
|
de79a46d43 | ||
|
|
bc19ef66bf | ||
|
|
de08f0afaa | ||
|
|
706bbeae16 | ||
|
|
e5497d89f4 | ||
|
|
048ec0aa66 | ||
|
|
03ed85b0a7 | ||
|
|
9d92707fd7 | ||
|
|
90c392e270 | ||
|
|
6cb6cbfefd | ||
|
|
6d2bca0fd1 | ||
|
|
99286391e1 | ||
|
|
711f2da496 | ||
|
|
fbd68b6f89 | ||
|
|
85ebd0ab59 | ||
|
|
58034219b6 | ||
|
|
17f5a466d9 | ||
|
|
90ca6a0998 | ||
|
|
ec2b433733 | ||
|
|
492c4b7f00 | ||
|
|
b3beb9f3c9 | ||
|
|
837e7affa7 | ||
|
|
6d527842dd | ||
|
|
e30915eb2c | ||
|
|
43e2b58f20 | ||
|
|
d3c6c892a8 | ||
|
|
a63edcf505 | ||
|
|
0e7088ce3b | ||
|
|
0042e7725d | ||
|
|
613f8d0bd2 | ||
|
|
61ca1ab2c1 | ||
|
|
ad62591f43 | ||
|
|
5ba33bc40e | ||
|
|
143b08d661 | ||
|
|
87a9fd8252 | ||
|
|
b1f7b5c6d7 | ||
|
|
bb97af1504 | ||
|
|
9a092654e9 | ||
|
|
d59b98ee2b | ||
|
|
6bbbbd9e17 | ||
|
|
9fbedd8b5f | ||
|
|
a91163877f | ||
|
|
0acce86596 | ||
|
|
c95b03f240 | ||
|
|
56ce3e5f5a | ||
|
|
8b5751ad44 | ||
|
|
c615272c06 | ||
|
|
a3b8122707 | ||
|
|
682e3460e0 | ||
|
|
71dbd10b39 | ||
|
|
7f143bcdf9 | ||
|
|
f19a46dcfe | ||
|
|
625f69443a | ||
|
|
0bdd293572 | ||
|
|
f8072aae68 | ||
|
|
92afcae9be | ||
|
|
a6f37c032b | ||
|
|
e66d15b71d | ||
|
|
1098194a89 | ||
|
|
66d23cd15f | ||
|
|
c3a1193ef9 | ||
|
|
ec2d9af8dc | ||
|
|
f0e44728d7 | ||
|
|
f67747456f | ||
|
|
f6017a17b2 | ||
|
|
89aa3cbc62 | ||
|
|
543190dfb0 | ||
|
|
fdc8c45a69 | ||
|
|
d2d421ca8f | ||
|
|
7ad5b3a17b | ||
|
|
c8dd9696b4 | ||
|
|
58ef69b95d | ||
|
|
0de9229d75 | ||
|
|
f4c3ac2a62 | ||
|
|
6c5ceaf686 | ||
|
|
54f65ae87d | ||
|
|
4f2dc3cc2a | ||
|
|
a4ee2bd8ef | ||
|
|
d4629a7efe | ||
|
|
441ae73344 | ||
|
|
9249dc6dd3 | ||
|
|
4c4539caff | ||
|
|
4c0ff0e0d0 | ||
|
|
1ceee2d6c5 | ||
|
|
cbb74d50ce | ||
|
|
c2d72bbf09 | ||
|
|
f97ba263c4 | ||
|
|
85df9e98bd | ||
|
|
2faafb9c0f | ||
|
|
d43101f22a | ||
|
|
3d23cd10fc | ||
|
|
66cd8d264e | ||
|
|
291910d74e | ||
|
|
7ac648d0ab | ||
|
|
3b42390062 | ||
|
|
a80917f530 | ||
|
|
8fbb585874 | ||
|
|
027d97321f | ||
|
|
62a1c9687e | ||
|
|
a9c6f8c1d9 | ||
|
|
06b4fcc2cf | ||
|
|
6dc2501116 | ||
|
|
54060f27ef | ||
|
|
85aa4fdd2e | ||
|
|
0624445627 | ||
|
|
4ca4941c82 | ||
|
|
4dbd84ead0 | ||
|
|
0495776a22 | ||
|
|
dd35551047 | ||
|
|
9f61369156 | ||
|
|
bd536be66d | ||
|
|
c5b69a0ee4 | ||
|
|
206b3a88a2 | ||
|
|
880bd011a9 | ||
|
|
7de91a270a | ||
|
|
09973abe8a | ||
|
|
342a819fd4 | ||
|
|
4bf185c868 | ||
|
|
78742c016b | ||
|
|
1ed314f6f6 | ||
|
|
be23c6c86d | ||
|
|
a194c4f1bd | ||
|
|
edb24add6b | ||
|
|
bc88985889 | ||
|
|
3d27dd3ec4 | ||
|
|
c2f5eb3073 | ||
|
|
541b268721 | ||
|
|
e77daed086 | ||
|
|
fdbb409331 | ||
|
|
f96c5aa62f | ||
|
|
c07a096e57 | ||
|
|
48b6c5b5cb | ||
|
|
d8d59d9a66 | ||
|
|
702dddbb2f | ||
|
|
127488004c | ||
|
|
7c925ac295 | ||
|
|
8617b92d1b | ||
|
|
bb80e3a9fc | ||
|
|
80023f62d9 | ||
|
|
9617288bd5 | ||
|
|
4fd79afa42 | ||
|
|
719f9a63d9 | ||
|
|
4bc33d0352 | ||
|
|
c4e1035638 | ||
|
|
d40e889d3b | ||
|
|
41acc8fa43 | ||
|
|
90c2aed7b4 | ||
|
|
5108602de3 | ||
|
|
5ee17ffc58 | ||
|
|
6b899ddc1d | ||
|
|
0a711922ef | ||
|
|
475b631d9c | ||
|
|
40c75f0a51 | ||
|
|
1dd99a6d5d | ||
|
|
bed9b038c8 | ||
|
|
9210c57c2d | ||
|
|
313cbda0aa | ||
|
|
86b0e49995 | ||
|
|
a39148dd38 | ||
|
|
b8c8c71b78 | ||
|
|
65a3bf2325 | ||
|
|
ac34db3c8a | ||
|
|
f2e86ecd8e | ||
|
|
7b993da0de | ||
|
|
de5bee6359 | ||
|
|
34f124190c | ||
|
|
d867366be1 | ||
|
|
d0d375d433 | ||
|
|
d17aa103b4 | ||
|
|
f0af23a4f5 | ||
|
|
bdd6bb7918 | ||
|
|
3ec49a0ef0 | ||
|
|
7a2d049ce3 | ||
|
|
7fba4b354e | ||
|
|
87cecd7e95 | ||
|
|
303cb8e350 | ||
|
|
236ae94474 | ||
|
|
308969e6dd | ||
|
|
4dd558a420 | ||
|
|
0e6a60b086 | ||
|
|
4fc0163139 | ||
|
|
314d34a644 | ||
|
|
2745b0f99e | ||
|
|
c2e8646aed | ||
|
|
e5919c1bfe | ||
|
|
c8961fcf99 | ||
|
|
58cee75c0e | ||
|
|
4f13236008 | ||
|
|
2a377a6125 | ||
|
|
058dba50cc | ||
|
|
e6846e7eb9 | ||
|
|
a0ddda4bc6 | ||
|
|
57c0f96118 | ||
|
|
a84429538b | ||
|
|
07953fb7e3 | ||
|
|
12da6f531e | ||
|
|
769f5aafb7 | ||
|
|
a1abab8ced | ||
|
|
270a998e3c | ||
|
|
d829497c3d | ||
|
|
a8d5b0e5ec | ||
|
|
85d732a45a | ||
|
|
8f3e8d29f0 | ||
|
|
ee4543d739 | ||
|
|
59456f20fb | ||
|
|
4a1f609893 | ||
|
|
36214c73ee | ||
|
|
20a1025a8c | ||
|
|
ec85884d92 | ||
|
|
22c01b956f | ||
|
|
9cdf84dacf | ||
|
|
60f40800c4 | ||
|
|
3b7b12bbd5 | ||
|
|
9a1aad8e92 | ||
|
|
6f398f59df | ||
|
|
8ace656657 | ||
|
|
a4481efe07 | ||
|
|
dc31ddbef2 | ||
|
|
0c2fe4c5f3 | ||
|
|
03febb81d3 | ||
|
|
6f1a25d8d6 | ||
|
|
f182f7dccd | ||
|
|
31fcd230b1 | ||
|
|
1ad5cae98e | ||
|
|
5af4864326 | ||
|
|
ea8d278f8f | ||
|
|
88de9908ce | ||
|
|
1bbecce5eb | ||
|
|
2495226c87 | ||
|
|
c1aa1fb0e0 | ||
|
|
a5b198e2b8 | ||
|
|
cd669239ae | ||
|
|
bc73a6829d | ||
|
|
542b640ef0 | ||
|
|
7823fb9788 | ||
|
|
bcb42674fc | ||
|
|
1f1a46a8bd | ||
|
|
7b999e6cd1 | ||
|
|
c97a25cc86 | ||
|
|
cdf24ec205 | ||
|
|
579f4d4cca | ||
|
|
fce04e7ad0 | ||
|
|
6c94650603 | ||
|
|
fca8ad5b0b | ||
|
|
4b4fb038e3 | ||
|
|
0b3a66dd1a | ||
|
|
2d8cf7de44 | ||
|
|
ad931eac60 | ||
|
|
438e78610d | ||
|
|
058f6f9303 | ||
|
|
73cdf00512 | ||
|
|
12b5caed70 | ||
|
|
18d2668e3b | ||
|
|
af21f72d17 | ||
|
|
b64680e4a8 | ||
|
|
9faedf0e67 | ||
|
|
204c151113 | ||
|
|
9b8256ec07 | ||
|
|
d61eb93c03 | ||
|
|
a2c6cde83d | ||
|
|
825c91f0c3 | ||
|
|
d406d7fa94 | ||
|
|
3db6faab4d | ||
|
|
15a046f20c | ||
|
|
d3cd304f68 | ||
|
|
84fb96a42f | ||
|
|
5a1fed3980 | ||
|
|
bb8af3a2d5 | ||
|
|
2803631906 | ||
|
|
4c47ed31ff | ||
|
|
1ed574b2a0 | ||
|
|
d69c1b848a | ||
|
|
35c29dac3f | ||
|
|
aac44f3a2b | ||
|
|
17dd8ddc9a | ||
|
|
a8b36d9baa | ||
|
|
520a8d0d0d | ||
|
|
033bd30391 | ||
|
|
99e14380fe | ||
|
|
5576649d60 | ||
|
|
82cd2f4ed6 | ||
|
|
66f12afbb1 | ||
|
|
431656bbcf | ||
|
|
a174a06e5c | ||
|
|
0d0fdb0adf | ||
|
|
5f8dd65acf | ||
|
|
601211f1d9 | ||
|
|
8983a97c70 | ||
|
|
2e899bd61c | ||
|
|
d0a8a57ae4 | ||
|
|
08aabd18ad | ||
|
|
2478623ebc | ||
|
|
736183e6f5 | ||
|
|
31f2707b2f | ||
|
|
d244d3b599 | ||
|
|
82904c59ce | ||
|
|
f8b2570cb3 | ||
|
|
6268840b55 | ||
|
|
c9ff0ab7eb | ||
|
|
cf1f9a29bf | ||
|
|
48b1f8e137 | ||
|
|
4b96a7c820 | ||
|
|
452ebdac8e | ||
|
|
7dc1499386 | ||
|
|
3ccc7787af | ||
|
|
02879e79e6 | ||
|
|
305c87a9c9 | ||
|
|
3abc78eef2 | ||
|
|
4a421e25b0 | ||
|
|
86047eceb1 | ||
|
|
7edbb6aadc | ||
|
|
0361f37178 | ||
|
|
55c5d254d5 | ||
|
|
36f5caa214 | ||
|
|
8c7898ed05 | ||
|
|
39de92960d | ||
|
|
6d35bdafee | ||
|
|
217ffc215b | ||
|
|
cbd3860585 | ||
|
|
5804dde0e9 | ||
|
|
e9b2cf1600 | ||
|
|
cdf2179b3e | ||
|
|
8f2ca856c7 | ||
|
|
622a6deb04 | ||
|
|
a698bc477d | ||
|
|
a36ae4b24a | ||
|
|
c703c89dbd | ||
|
|
b3227e491b | ||
|
|
9e83a80215 | ||
|
|
9c85702c87 | ||
|
|
9b7b39055d | ||
|
|
11f32d0500 | ||
|
|
326e26fbeb | ||
|
|
394c87c40b | ||
|
|
ce152e9c94 | ||
|
|
9e0946b207 | ||
|
|
da77edf8fc | ||
|
|
6cb991d583 | ||
|
|
f8e5df237b | ||
|
|
5d953061e8 | ||
|
|
1bcca8cba1 | ||
|
|
bdb6182921 | ||
|
|
55d1ad94ef | ||
|
|
6773c35760 | ||
|
|
5f89b34831 | ||
|
|
90ae5c6646 | ||
|
|
a8bb75d070 | ||
|
|
d82859b6ea | ||
|
|
7d41ce4e46 | ||
|
|
c23375a18b | ||
|
|
4c4e5d5f47 | ||
|
|
6ff24ed047 | ||
|
|
69ed6fe6e7 | ||
|
|
4e2d75a8f4 | ||
|
|
ae0dbbcfa5 | ||
|
|
25e1432403 | ||
|
|
adfcfad488 | ||
|
|
b0734e613f | ||
|
|
913c5ab47c | ||
|
|
429904c437 | ||
|
|
41a36df801 | ||
|
|
56a2ffca1d | ||
|
|
6e2fb17f19 | ||
|
|
586be7fad1 | ||
|
|
d9b30d1421 | ||
|
|
e9059a3ed9 | ||
|
|
473d6b1d05 | ||
|
|
7f17a50b4a | ||
|
|
c7183a14a5 | ||
|
|
c1eaf60461 | ||
|
|
ca6b957839 | ||
|
|
680385df93 | ||
|
|
62f21c3ac6 | ||
|
|
acfbbb3898 | ||
|
|
4403fe941d | ||
|
|
244fde880e | ||
|
|
df99e4ef22 | ||
|
|
976d9f2d08 | ||
|
|
f5dd146676 | ||
|
|
7b00b19223 | ||
|
|
b114ba56ea | ||
|
|
0d32bd7a19 | ||
|
|
4feef3dd0d | ||
|
|
9c3b1b7a96 | ||
|
|
a09d731058 | ||
|
|
27e35d5f34 | ||
|
|
c8fa6cc127 | ||
|
|
468087e6bc | ||
|
|
d4b6a7343f | ||
|
|
87c88078c8 | ||
|
|
8c1ebde1de | ||
|
|
ab5a3f9de3 | ||
|
|
eb3da8cb03 | ||
|
|
9e1ecd7124 | ||
|
|
efbaf47dc7 | ||
|
|
f10ecb2a8d | ||
|
|
6e81391711 | ||
|
|
0d64f4a2d5 | ||
|
|
d63e5a60ae | ||
|
|
384f1344fd | ||
|
|
add24915a3 | ||
|
|
089bbfc5cc | ||
|
|
1944b6a335 | ||
|
|
b8cfe63fc6 | ||
|
|
9e091a2c92 | ||
|
|
41bcdc3356 | ||
|
|
40486676df | ||
|
|
6f2078cda3 | ||
|
|
b8d2f2ba37 | ||
|
|
03491fcc09 | ||
|
|
571073fe1f | ||
|
|
a577d5c5fa | ||
|
|
160814f425 | ||
|
|
b83b36274a | ||
|
|
f3db4306c2 | ||
|
|
5c7fb5d7ae | ||
|
|
0dfc1c4e7a | ||
|
|
3f151428b7 | ||
|
|
2606e4d641 | ||
|
|
be25ea4f09 | ||
|
|
77959341a3 | ||
|
|
9f54bcc21b | ||
|
|
6d236b8169 | ||
|
|
09b894a4aa | ||
|
|
1e2b5e6991 | ||
|
|
496ec4bcca | ||
|
|
3e51d0b539 | ||
|
|
c31a291a9c | ||
|
|
96938bb33c | ||
|
|
e7b1682a4e | ||
|
|
d191635fd8 | ||
|
|
bdd945c1c4 | ||
|
|
6ccf63dae2 | ||
|
|
987282da78 | ||
|
|
56186232f3 | ||
|
|
fb2da6be9a | ||
|
|
8796187389 | ||
|
|
edff53609f | ||
|
|
dbc91c1d48 | ||
|
|
0607c8f4b4 | ||
|
|
7ccb6b960c | ||
|
|
561a78bef3 | ||
|
|
9876a2a081 | ||
|
|
fba5becd90 | ||
|
|
d5179b4bdc | ||
|
|
ff8f22854c | ||
|
|
110d721c76 | ||
|
|
8c010c8df4 | ||
|
|
0ac1759395 | ||
|
|
07fb4ff243 | ||
|
|
2650d235ea | ||
|
|
9e89197284 | ||
|
|
8159d36114 | ||
|
|
cf4f4ce8c7 | ||
|
|
9a1883bb49 | ||
|
|
8e16a443e5 | ||
|
|
35411cd57e | ||
|
|
845926236e | ||
|
|
eae07c070a | ||
|
|
659226886f | ||
|
|
1e52d5c7f2 | ||
|
|
9463c84603 | ||
|
|
7cc707f1ce | ||
|
|
59524c7933 | ||
|
|
1d141566bd | ||
|
|
ac41f3028c | ||
|
|
750ca79ac0 | ||
|
|
71bf707bcf | ||
|
|
31f1e1d7a4 | ||
|
|
361ab0f92b | ||
|
|
e68a8f9c0f | ||
|
|
15770ff90f | ||
|
|
618ebfe43c | ||
|
|
ff15fea9f8 | ||
|
|
2872c89f0c | ||
|
|
2dc9bc98f7 | ||
|
|
72d7e6e9dd | ||
|
|
f912daf4b2 | ||
|
|
d9fc2a8bf6 | ||
|
|
45fe37a301 | ||
|
|
ef76047ba2 | ||
|
|
d475e5362b | ||
|
|
3027b4a5a8 | ||
|
|
3829abbd2d | ||
|
|
e6e3b37a62 | ||
|
|
ce501ae627 | ||
|
|
b6f954e082 | ||
|
|
b368388714 | ||
|
|
8842e4e94f | ||
|
|
0ed608abff | ||
|
|
afcf3eaac3 |
51
.coveragerc
51
.coveragerc
@@ -5,9 +5,21 @@ omit =
|
||||
homeassistant/__main__.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
homeassistant/components/apcupsd.py
|
||||
homeassistant/components/*/apcupsd.py
|
||||
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
@@ -15,6 +27,10 @@ omit =
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
homeassistant/components/*/tellstick.py
|
||||
|
||||
homeassistant/components/tellduslive.py
|
||||
homeassistant/components/*/tellduslive.py
|
||||
|
||||
homeassistant/components/*/vera.py
|
||||
|
||||
homeassistant/components/ecobee.py
|
||||
@@ -26,12 +42,27 @@ omit =
|
||||
homeassistant/components/wink.py
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zwave.py
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/rfxtrx.py
|
||||
homeassistant/components/*/rfxtrx.py
|
||||
|
||||
homeassistant/components/mysensors.py
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
homeassistant/components/nest.py
|
||||
homeassistant/components/*/nest.py
|
||||
|
||||
homeassistant/components/rpi_gpio.py
|
||||
homeassistant/components/*/rpi_gpio.py
|
||||
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
@@ -41,12 +72,10 @@ omit =
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/geofancy.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/owntracks.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
@@ -54,12 +83,13 @@ omit =
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/influxdb.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
@@ -68,17 +98,23 @@ omit =
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/samsungtv.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/googlevoice.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
homeassistant/components/notify/pushetta.py
|
||||
homeassistant/components/notify/pushover.py
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
@@ -89,11 +125,12 @@ omit =
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/mysensors.py
|
||||
homeassistant/components/sensor/netatmo.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/rpi_gpio.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
@@ -108,16 +145,14 @@ omit =
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_gpio.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wemo.py
|
||||
homeassistant/components/thermostat/heatmiser.py
|
||||
homeassistant/components/thermostat/homematic.py
|
||||
homeassistant/components/thermostat/honeywell.py
|
||||
homeassistant/components/thermostat/nest.py
|
||||
homeassistant/components/thermostat/proliphix.py
|
||||
homeassistant/components/thermostat/radiotherm.py
|
||||
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -41,6 +41,7 @@ Icon
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
.eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
@@ -68,6 +69,16 @@ nosetests.xml
|
||||
|
||||
.python-version
|
||||
|
||||
# emacs auto backups
|
||||
*~
|
||||
*#
|
||||
*.orig
|
||||
|
||||
# venv stuff
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv
|
||||
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
@@ -8,9 +8,7 @@ python:
|
||||
- 3.4
|
||||
- 3.5
|
||||
install:
|
||||
# Validate requirements_all.txt on Python 3.5
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py develop; script/gen_requirements_all.py validate; fi
|
||||
- script/bootstrap_server
|
||||
- "true"
|
||||
script:
|
||||
- script/cibuild
|
||||
matrix:
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Paulus Schoutsen
|
||||
Copyright (c) 2016 Paulus Schoutsen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
@@ -29,9 +29,13 @@ import time
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.event_decorators import \
|
||||
track_state_change, track_time_change
|
||||
from homeassistant.helpers.service import service
|
||||
import homeassistant.components as core
|
||||
from homeassistant.components import device_tracker
|
||||
from homeassistant.components import light
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component
|
||||
DOMAIN = "example"
|
||||
@@ -39,11 +43,14 @@ DOMAIN = "example"
|
||||
# List of component names (string) your component depends upon
|
||||
# We depend on group because group will be loaded after all the components that
|
||||
# initialize devices have been setup.
|
||||
DEPENDENCIES = ['group']
|
||||
DEPENDENCIES = ['group', 'device_tracker', 'light']
|
||||
|
||||
# Configuration key for the entity id we are targetting
|
||||
CONF_TARGET = 'target'
|
||||
|
||||
# Variable for storing configuration parameters
|
||||
TARGET_ID = None
|
||||
|
||||
# Name of the service that we expose
|
||||
SERVICE_FLASH = 'flash'
|
||||
|
||||
@@ -53,84 +60,89 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup example component. """
|
||||
global TARGET_ID
|
||||
|
||||
# Validate that all required config options are given
|
||||
if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER):
|
||||
return False
|
||||
|
||||
target_id = config[DOMAIN][CONF_TARGET]
|
||||
TARGET_ID = config[DOMAIN][CONF_TARGET]
|
||||
|
||||
# Validate that the target entity id exists
|
||||
if hass.states.get(target_id) is None:
|
||||
_LOGGER.error("Target entity id %s does not exist", target_id)
|
||||
if hass.states.get(TARGET_ID) is None:
|
||||
_LOGGER.error("Target entity id %s does not exist",
|
||||
TARGET_ID)
|
||||
|
||||
# Tell the bootstrapper that we failed to initialize
|
||||
# Tell the bootstrapper that we failed to initialize and clear the
|
||||
# stored target id so our functions don't run.
|
||||
TARGET_ID = None
|
||||
return False
|
||||
|
||||
# We will use the component helper methods to check the states.
|
||||
device_tracker = loader.get_component('device_tracker')
|
||||
light = loader.get_component('light')
|
||||
|
||||
def track_devices(entity_id, old_state, new_state):
|
||||
""" Called when the group.all devices change state. """
|
||||
|
||||
# If anyone comes home and the core is not on, turn it on.
|
||||
if new_state.state == STATE_HOME and not core.is_on(hass, target_id):
|
||||
|
||||
core.turn_on(hass, target_id)
|
||||
|
||||
# If all people leave the house and the core is on, turn it off
|
||||
elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id):
|
||||
|
||||
core.turn_off(hass, target_id)
|
||||
|
||||
# Register our track_devices method to receive state changes of the
|
||||
# all tracked devices group.
|
||||
hass.states.track_change(
|
||||
device_tracker.ENTITY_ID_ALL_DEVICES, track_devices)
|
||||
|
||||
def wake_up(now):
|
||||
""" Turn it on in the morning if there are people home and
|
||||
it is not already on. """
|
||||
|
||||
if device_tracker.is_on(hass) and not core.is_on(hass, target_id):
|
||||
_LOGGER.info('People home at 7AM, turning it on')
|
||||
core.turn_on(hass, target_id)
|
||||
|
||||
# Register our wake_up service to be called at 7AM in the morning
|
||||
hass.track_time_change(wake_up, hour=7, minute=0, second=0)
|
||||
|
||||
def all_lights_off(entity_id, old_state, new_state):
|
||||
""" If all lights turn off, turn off. """
|
||||
|
||||
if core.is_on(hass, target_id):
|
||||
_LOGGER.info('All lights have been turned off, turning it off')
|
||||
core.turn_off(hass, target_id)
|
||||
|
||||
# Register our all_lights_off method to be called when all lights turn off
|
||||
hass.states.track_change(
|
||||
light.ENTITY_ID_ALL_LIGHTS, all_lights_off, STATE_ON, STATE_OFF)
|
||||
|
||||
def flash_service(call):
|
||||
""" Service that will turn the target off for 10 seconds
|
||||
if on and vice versa. """
|
||||
|
||||
if core.is_on(hass, target_id):
|
||||
core.turn_off(hass, target_id)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_on(hass, target_id)
|
||||
|
||||
else:
|
||||
core.turn_on(hass, target_id)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_off(hass, target_id)
|
||||
|
||||
# Register our service with HASS.
|
||||
hass.services.register(DOMAIN, SERVICE_FLASH, flash_service)
|
||||
|
||||
# Tells the bootstrapper that the component was successfully initialized
|
||||
# Tell the bootstrapper that we initialized successfully
|
||||
return True
|
||||
|
||||
|
||||
@track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES)
|
||||
def track_devices(hass, entity_id, old_state, new_state):
|
||||
""" Called when the group.all devices change state. """
|
||||
# If the target id is not set, return
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
# If anyone comes home and the entity is not on, turn it on.
|
||||
if new_state.state == STATE_HOME and not core.is_on(hass, TARGET_ID):
|
||||
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
# If all people leave the house and the entity is on, turn it off
|
||||
elif new_state.state == STATE_NOT_HOME and core.is_on(hass, TARGET_ID):
|
||||
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
|
||||
@track_time_change(hour=7, minute=0, second=0)
|
||||
def wake_up(hass, now):
|
||||
"""
|
||||
Turn it on in the morning (7 AM) if there are people home and
|
||||
it is not already on.
|
||||
"""
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
if device_tracker.is_on(hass) and not core.is_on(hass, TARGET_ID):
|
||||
_LOGGER.info('People home at 7AM, turning it on')
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
|
||||
@track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF)
|
||||
def all_lights_off(hass, entity_id, old_state, new_state):
|
||||
""" If all lights turn off, turn off. """
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
if core.is_on(hass, TARGET_ID):
|
||||
_LOGGER.info('All lights have been turned off, turning it off')
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
|
||||
@service(DOMAIN, SERVICE_FLASH)
|
||||
def flash_service(hass, call):
|
||||
"""
|
||||
Service that will turn the target off for 10 seconds if on and vice versa.
|
||||
"""
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
if core.is_on(hass, TARGET_ID):
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
else:
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
""" Starts home assistant. """
|
||||
from __future__ import print_function
|
||||
|
||||
from multiprocessing import Process
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import os
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from homeassistant import bootstrap
|
||||
import homeassistant.config as config_util
|
||||
from homeassistant.const import __version__, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.const import (__version__, EVENT_HOMEASSISTANT_START,
|
||||
RESTART_EXIT_CODE)
|
||||
|
||||
|
||||
def validate_python():
|
||||
@@ -73,6 +78,11 @@ def get_arguments():
|
||||
'--demo-mode',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in demo mode')
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in debug mode. Runs in single process to '
|
||||
'enable use of interactive debuggers.')
|
||||
parser.add_argument(
|
||||
'--open-ui',
|
||||
action='store_true',
|
||||
@@ -204,35 +214,11 @@ def uninstall_osx():
|
||||
print("Home Assistant has been uninstalled.")
|
||||
|
||||
|
||||
def main():
|
||||
""" Starts Home Assistant. """
|
||||
validate_python()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
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
|
||||
if args.uninstall_osx:
|
||||
uninstall_osx()
|
||||
return
|
||||
if args.restart_osx:
|
||||
uninstall_osx()
|
||||
install_osx()
|
||||
return
|
||||
|
||||
# daemon functions
|
||||
if args.pid_file:
|
||||
check_pid(args.pid_file)
|
||||
if args.daemon:
|
||||
daemonize()
|
||||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
def setup_and_run_hass(config_dir, args, top_process=False):
|
||||
"""
|
||||
Setup HASS and run. Block until stopped. Will assume it is running in a
|
||||
subprocess unless top_process is set to true.
|
||||
"""
|
||||
if args.demo_mode:
|
||||
config = {
|
||||
'frontend': {},
|
||||
@@ -259,7 +245,91 @@ def main():
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||
|
||||
hass.start()
|
||||
hass.block_till_stopped()
|
||||
exit_code = int(hass.block_till_stopped())
|
||||
|
||||
if not top_process:
|
||||
sys.exit(exit_code)
|
||||
return exit_code
|
||||
|
||||
|
||||
def run_hass_process(hass_proc):
|
||||
""" Runs a child hass process. Returns True if it should be restarted. """
|
||||
requested_stop = threading.Event()
|
||||
hass_proc.daemon = True
|
||||
|
||||
def request_stop(*args):
|
||||
""" request hass stop, *args is for signal handler callback """
|
||||
requested_stop.set()
|
||||
hass_proc.terminate()
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
except ValueError:
|
||||
print('Could not bind to SIGTERM. Are you running in a thread?')
|
||||
|
||||
hass_proc.start()
|
||||
try:
|
||||
hass_proc.join()
|
||||
except KeyboardInterrupt:
|
||||
request_stop()
|
||||
try:
|
||||
hass_proc.join()
|
||||
except KeyboardInterrupt:
|
||||
return False
|
||||
|
||||
return (not requested_stop.isSet() and
|
||||
hass_proc.exitcode == RESTART_EXIT_CODE,
|
||||
hass_proc.exitcode)
|
||||
|
||||
|
||||
def main():
|
||||
""" Starts Home Assistant. """
|
||||
validate_python()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
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)
|
||||
if args.daemon:
|
||||
daemonize()
|
||||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
# Run hass in debug mode if requested
|
||||
if args.debug:
|
||||
sys.stderr.write('Running in debug mode. '
|
||||
'Home Assistant will not be able to restart.\n')
|
||||
exit_code = setup_and_run_hass(config_dir, args, top_process=True)
|
||||
if exit_code == RESTART_EXIT_CODE:
|
||||
sys.stderr.write('Home Assistant requested a '
|
||||
'restart in debug mode.\n')
|
||||
return exit_code
|
||||
|
||||
# Run hass as child process. Restart if necessary.
|
||||
keep_running = True
|
||||
while keep_running:
|
||||
hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args))
|
||||
keep_running, exit_code = run_hass_process(hass_proc)
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
||||
@@ -24,6 +24,7 @@ import homeassistant.config as config_util
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.components as core_components
|
||||
import homeassistant.components.group as group
|
||||
from homeassistant.helpers import event_decorators, service
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
__version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
|
||||
@@ -199,6 +200,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
|
||||
# Setup the components
|
||||
for domain in loader.load_order_components(components):
|
||||
_setup_component(hass, domain, config)
|
||||
@@ -223,7 +228,7 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False,
|
||||
|
||||
enable_logging(hass, verbose, daemon, log_rotate_days)
|
||||
|
||||
config_dict = config_util.load_config_file(config_path)
|
||||
config_dict = config_util.load_yaml_config_file(config_path)
|
||||
|
||||
return from_config_dict(config_dict, hass, enable_log=False,
|
||||
skip_pip=skip_pip)
|
||||
@@ -275,7 +280,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
|
||||
datefmt='%y-%m-%d %H:%M:%S'))
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(err_handler)
|
||||
logger.setLevel(logging.NOTSET) # this sets the minimum log level
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
else:
|
||||
_LOGGER.error(
|
||||
|
||||
@@ -16,11 +16,11 @@ import itertools as it
|
||||
import logging
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers import extract_entity_ids
|
||||
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 (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,7 +36,7 @@ def is_on(hass, entity_id=None):
|
||||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = util.split_entity_id(entity_id)[0]
|
||||
domain = split_entity_id(entity_id)[0]
|
||||
|
||||
module = get_component(domain)
|
||||
|
||||
@@ -68,6 +68,14 @@ def turn_off(hass, entity_id=None, **service_data):
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
|
||||
|
||||
|
||||
def toggle(hass, entity_id=None, **service_data):
|
||||
""" Toggles specified entity. """
|
||||
if entity_id is not None:
|
||||
service_data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup general services related to homeassistant. """
|
||||
|
||||
@@ -84,18 +92,27 @@ def setup(hass, config):
|
||||
|
||||
# Group entity_ids by domain. groupby requires sorted data.
|
||||
by_domain = it.groupby(sorted(entity_ids),
|
||||
lambda item: util.split_entity_id(item)[0])
|
||||
lambda item: 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
|
||||
# have been processed. If a service does not exist it causes a 10
|
||||
# second delay while we're blocking waiting for a response.
|
||||
# But services can be registered on other HA instances that are
|
||||
# listening to the bus too. So as a in between solution, we'll
|
||||
# block only if the service is defined in the current HA instance.
|
||||
blocking = hass.services.has_service(domain, service.service)
|
||||
|
||||
# Create a new dict for this call
|
||||
data = dict(service.data)
|
||||
|
||||
# ent_ids is a generator, convert it to a list.
|
||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||
|
||||
hass.services.call(domain, service.service, data, True)
|
||||
hass.services.call(domain, service.service, data, blocking)
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
|
||||
return True
|
||||
|
||||
@@ -61,6 +61,8 @@ def setup(hass, config):
|
||||
|
||||
for alarm in target_alarms:
|
||||
getattr(alarm, method)(code)
|
||||
if alarm.should_poll:
|
||||
alarm.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
117
homeassistant/components/alarm_control_panel/alarmdotcom.py
Normal file
117
homeassistant/components/alarm_control_panel/alarmdotcom.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel.alarmdotcom
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Interfaces with Verisure alarm control panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom'
|
||||
'/archive/0.0.7.zip'
|
||||
'#pyalarmdotcom==0.0.7']
|
||||
DEFAULT_NAME = 'Alarm.com'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Setup an Alarm.com control panel. """
|
||||
|
||||
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([AlarmDotCom(hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('code'),
|
||||
username,
|
||||
password)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
""" Represents a Alarm.com status. """
|
||||
|
||||
def __init__(self, hass, name, code, username, password):
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
self._alarm = Alarmdotcom(username, password, timeout=10)
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@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):
|
||||
""" Returns the state of the device. """
|
||||
if self._alarm.state == 'Disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
elif self._alarm.state == 'Armed Stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state == 'Armed Away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.disarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_stay()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_away()
|
||||
|
||||
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
|
||||
@@ -68,7 +68,8 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self._state in (STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) and \
|
||||
if self._state in (STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY) and \
|
||||
self._pending_time and self._state_ts + self._pending_time > \
|
||||
dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
105
homeassistant/components/alarm_control_panel/nx584.py
Normal file
105
homeassistant/components/alarm_control_panel/nx584.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel.nx584
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for NX584 alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.nx584/
|
||||
"""
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from homeassistant.const import (STATE_UNKNOWN, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY)
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Setup nx584. """
|
||||
host = config.get('host', 'localhost:5007')
|
||||
|
||||
try:
|
||||
add_devices([NX584Alarm(hass, host, config.get('name', 'NX584'))])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to NX584: %s', str(ex))
|
||||
return False
|
||||
|
||||
|
||||
class NX584Alarm(alarm.AlarmControlPanel):
|
||||
""" NX584-based alarm panel. """
|
||||
def __init__(self, hass, host, name):
|
||||
from nx584 import client
|
||||
self._hass = hass
|
||||
self._host = host
|
||||
self._name = name
|
||||
self._alarm = client.Client('http://%s' % host)
|
||||
# Do an initial list operation so that we will try to actually
|
||||
# talk to the API and trigger a requests exception for setup_platform()
|
||||
# to catch
|
||||
self._alarm.list_zones()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Polling needed. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" Characters if code is defined. """
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
zones = self._alarm.list_zones()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||
dict(host=self._host, reason=ex))
|
||||
return STATE_UNKNOWN
|
||||
except IndexError:
|
||||
_LOGGER.error('nx584 reports no partitions')
|
||||
return STATE_UNKNOWN
|
||||
|
||||
bypassed = False
|
||||
for zone in zones:
|
||||
if zone['bypassed']:
|
||||
_LOGGER.debug('Zone %(zone)s is bypassed, '
|
||||
'assuming HOME',
|
||||
dict(zone=zone['number']))
|
||||
bypassed = True
|
||||
break
|
||||
|
||||
if not part['armed']:
|
||||
return STATE_ALARM_DISARMED
|
||||
elif bypassed:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
self._alarm.disarm(code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
self._alarm.arm('home')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
self._alarm.arm('auto')
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
""" Alarm trigger command. """
|
||||
raise NotImplementedError()
|
||||
@@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
alarms.extend([
|
||||
VerisureAlarm(value)
|
||||
for value in verisure.get_alarm_status().values()
|
||||
for value in verisure.ALARM_STATUS.values()
|
||||
if verisure.SHOW_ALARM
|
||||
])
|
||||
|
||||
@@ -42,7 +42,6 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def __init__(self, alarm_status):
|
||||
self._id = alarm_status.id
|
||||
self._device = verisure.MY_PAGES.DEVICE_ALARM
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
@@ -58,40 +57,40 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
""" Four digit code required. """
|
||||
return '^\\d{4}$'
|
||||
return '^\\d{%s}$' % verisure.CODE_DIGITS
|
||||
|
||||
def update(self):
|
||||
""" Update alarm status """
|
||||
verisure.update()
|
||||
verisure.update_alarm()
|
||||
|
||||
if verisure.STATUS[self._device][self._id].status == 'unarmed':
|
||||
if verisure.ALARM_STATUS[self._id].status == 'unarmed':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedhome':
|
||||
elif verisure.ALARM_STATUS[self._id].status == 'armedhome':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedaway':
|
||||
elif verisure.ALARM_STATUS[self._id].status == 'armed':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif verisure.STATUS[self._device][self._id].status != 'pending':
|
||||
elif verisure.ALARM_STATUS[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
'Unknown alarm state %s',
|
||||
verisure.STATUS[self._device][self._id].status)
|
||||
verisure.ALARM_STATUS[self._id].status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_DISARMED)
|
||||
_LOGGER.warning('disarming')
|
||||
verisure.MY_PAGES.alarm.set(code, 'DISARMED')
|
||||
_LOGGER.info('verisure alarm disarming')
|
||||
verisure.MY_PAGES.alarm.wait_while_pending()
|
||||
verisure.update_alarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_ARMED_HOME)
|
||||
_LOGGER.warning('arming home')
|
||||
verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME')
|
||||
_LOGGER.info('verisure alarm arming home')
|
||||
verisure.MY_PAGES.alarm.wait_while_pending()
|
||||
verisure.update_alarm()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_ARMED_AWAY)
|
||||
_LOGGER.warning('arming away')
|
||||
verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY')
|
||||
_LOGGER.info('verisure alarm arming away')
|
||||
verisure.MY_PAGES.alarm.wait_while_pending()
|
||||
verisure.update_alarm()
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
|
||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||
from homeassistant.util import template
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -23,6 +24,7 @@ API_ENDPOINT = '/api/alexa'
|
||||
CONF_INTENTS = 'intents'
|
||||
CONF_CARD = 'card'
|
||||
CONF_SPEECH = 'speech'
|
||||
CONF_ACTION = 'action'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@@ -80,6 +82,7 @@ def _handle_alexa(handler, path_match, data):
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
@@ -89,6 +92,9 @@ def _handle_alexa(handler, path_match, data):
|
||||
response.add_card(CardType[card['type']], card['title'],
|
||||
card['content'])
|
||||
|
||||
if action is not None:
|
||||
call_from_config(handler.server.hass, action, True)
|
||||
|
||||
handler.write_json(response.as_dict())
|
||||
|
||||
|
||||
@@ -116,7 +122,7 @@ class AlexaResponse(object):
|
||||
self.should_end_session = True
|
||||
if intent is not None and 'slots' in intent:
|
||||
self.variables = {key: value['value'] for key, value
|
||||
in intent['slots'].items()}
|
||||
in intent['slots'].items() if 'value' in value}
|
||||
else:
|
||||
self.variables = {}
|
||||
|
||||
|
||||
84
homeassistant/components/apcupsd.py
Normal file
84
homeassistant/components/apcupsd.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
homeassistant.components.apcupsd
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Sets up and provides access to the status output of APCUPSd via its Network
|
||||
Information Server (NIS).
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/apcupsd/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = "apcupsd"
|
||||
REQUIREMENTS = ("apcaccess==0.0.4",)
|
||||
|
||||
CONF_HOST = "host"
|
||||
CONF_PORT = "port"
|
||||
CONF_TYPE = "type"
|
||||
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_PORT = 3551
|
||||
|
||||
KEY_STATUS = "STATUS"
|
||||
|
||||
VALUE_ONLINE = "ONLINE"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
DATA = None
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Use config values to set up a function enabling status retrieval. """
|
||||
global DATA
|
||||
|
||||
host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST)
|
||||
port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT)
|
||||
|
||||
DATA = APCUPSdData(host, port)
|
||||
|
||||
# It doesn't really matter why we're not able to get the status, just that
|
||||
# we can't.
|
||||
# pylint: disable=broad-except
|
||||
try:
|
||||
DATA.update(no_throttle=True)
|
||||
except Exception:
|
||||
_LOGGER.exception("Failure while testing APCUPSd status retrieval.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class APCUPSdData(object):
|
||||
"""
|
||||
Stores the data retrieved from APCUPSd for each entity to use, acts as the
|
||||
single point responsible for fetching updates from the server.
|
||||
"""
|
||||
def __init__(self, host, port):
|
||||
from apcaccess import status
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._status = None
|
||||
self._get = status.get
|
||||
self._parse = status.parse
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
""" Get latest update if throttle allows. Return status. """
|
||||
self.update()
|
||||
return self._status
|
||||
|
||||
def _get_status(self):
|
||||
""" Get the status from APCUPSd and parse it into a dict. """
|
||||
return self._parse(self._get(host=self._host, port=self._port))
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self, **kwargs):
|
||||
"""
|
||||
Fetch the latest status from APCUPSd and store it in self._status.
|
||||
"""
|
||||
self._status = self._get_status()
|
||||
@@ -9,11 +9,6 @@ https://home-assistant.io/components/arduino/
|
||||
"""
|
||||
import logging
|
||||
|
||||
try:
|
||||
from PyMata.pymata import PyMata
|
||||
except ImportError:
|
||||
PyMata = None
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
@@ -27,18 +22,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def setup(hass, config):
|
||||
""" Setup the Arduino component. """
|
||||
|
||||
global PyMata # pylint: disable=invalid-name
|
||||
if PyMata is None:
|
||||
from PyMata.pymata import PyMata as PyMata_
|
||||
PyMata = PyMata_
|
||||
|
||||
import serial
|
||||
|
||||
if not validate_config(config,
|
||||
{DOMAIN: ['port']},
|
||||
_LOGGER):
|
||||
return False
|
||||
|
||||
import serial
|
||||
global BOARD
|
||||
try:
|
||||
BOARD = ArduinoBoard(config[DOMAIN]['port'])
|
||||
@@ -67,6 +56,7 @@ class ArduinoBoard(object):
|
||||
""" Represents an Arduino board. """
|
||||
|
||||
def __init__(self, port):
|
||||
from PyMata.pymata import PyMata
|
||||
self._port = port
|
||||
self._board = PyMata(self._port, verbose=False)
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ https://home-assistant.io/components/automation/
|
||||
import logging
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.util import split_entity_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
|
||||
DOMAIN = 'automation'
|
||||
|
||||
@@ -19,8 +19,6 @@ DEPENDENCIES = ['group']
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
CONF_SERVICE = 'service'
|
||||
CONF_SERVICE_ENTITY_ID = 'entity_id'
|
||||
CONF_SERVICE_DATA = 'data'
|
||||
|
||||
CONF_CONDITION = 'condition'
|
||||
CONF_ACTION = 'action'
|
||||
@@ -96,22 +94,7 @@ def _get_action(hass, config, name):
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
service_data = config.get(CONF_SERVICE_DATA, {})
|
||||
|
||||
if not isinstance(service_data, dict):
|
||||
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
|
||||
service_data = {}
|
||||
|
||||
if CONF_SERVICE_ENTITY_ID in config:
|
||||
try:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID].split(",")
|
||||
except AttributeError:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID]
|
||||
|
||||
hass.services.call(domain, service, service_data)
|
||||
call_from_config(hass, config)
|
||||
|
||||
return action
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Offers numeric state listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
||||
"""
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
@@ -20,6 +21,14 @@ CONF_ABOVE = "above"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _renderer(hass, value_template, state):
|
||||
"""Render state value."""
|
||||
if value_template is None:
|
||||
return state.state
|
||||
|
||||
return template.render(hass, value_template, {'state': state})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
@@ -38,12 +47,7 @@ def trigger(hass, config, action):
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return False
|
||||
|
||||
if value_template is not None:
|
||||
renderer = lambda value: template.render(hass,
|
||||
value_template,
|
||||
{'state': value})
|
||||
else:
|
||||
renderer = lambda value: value.state
|
||||
renderer = partial(_renderer, hass, value_template)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
@@ -79,12 +83,7 @@ def if_action(hass, config):
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return None
|
||||
|
||||
if value_template is not None:
|
||||
renderer = lambda value: template.render(hass,
|
||||
value_template,
|
||||
{'state': value})
|
||||
else:
|
||||
renderer = lambda value: value.state
|
||||
renderer = partial(_renderer, hass, value_template)
|
||||
|
||||
def if_numeric_state():
|
||||
""" Test numeric state condition. """
|
||||
|
||||
@@ -10,13 +10,17 @@ import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components import sun
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.helpers.event import track_sunrise, track_sunset
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
DEPENDENCIES = ['sun']
|
||||
|
||||
CONF_OFFSET = 'offset'
|
||||
CONF_EVENT = 'event'
|
||||
CONF_BEFORE = "before"
|
||||
CONF_BEFORE_OFFSET = "before_offset"
|
||||
CONF_AFTER = "after"
|
||||
CONF_AFTER_OFFSET = "after_offset"
|
||||
|
||||
EVENT_SUNSET = 'sunset'
|
||||
EVENT_SUNRISE = 'sunrise'
|
||||
@@ -37,69 +41,108 @@ def trigger(hass, config, action):
|
||||
_LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event)
|
||||
return False
|
||||
|
||||
if CONF_OFFSET in config:
|
||||
raw_offset = config.get(CONF_OFFSET)
|
||||
|
||||
negative_offset = False
|
||||
if raw_offset.startswith('-'):
|
||||
negative_offset = True
|
||||
raw_offset = raw_offset[1:]
|
||||
|
||||
try:
|
||||
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
|
||||
except ValueError:
|
||||
_LOGGER.error('Could not parse offset %s', raw_offset)
|
||||
return False
|
||||
|
||||
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
if negative_offset:
|
||||
offset *= -1
|
||||
else:
|
||||
offset = timedelta(0)
|
||||
offset = _parse_offset(config.get(CONF_OFFSET))
|
||||
if offset is False:
|
||||
return False
|
||||
|
||||
# Do something to call action
|
||||
if event == EVENT_SUNRISE:
|
||||
trigger_sunrise(hass, action, offset)
|
||||
track_sunrise(hass, action, offset)
|
||||
else:
|
||||
trigger_sunset(hass, action, offset)
|
||||
track_sunset(hass, action, offset)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def trigger_sunrise(hass, action, offset):
|
||||
""" Trigger action at next sun rise. """
|
||||
def next_rise():
|
||||
""" Returns next sunrise. """
|
||||
next_time = sun.next_rising_utc(hass) + offset
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with sun based condition. """
|
||||
before = config.get(CONF_BEFORE)
|
||||
after = config.get(CONF_AFTER)
|
||||
|
||||
while next_time < dt_util.utcnow():
|
||||
next_time = next_time + timedelta(days=1)
|
||||
# Make sure required configuration keys are present
|
||||
if before is None and after is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing if-condition configuration key %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER)
|
||||
return None
|
||||
|
||||
return next_time
|
||||
# Make sure configuration keys have the right value
|
||||
if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \
|
||||
after not in (None, EVENT_SUNRISE, EVENT_SUNSET):
|
||||
logging.getLogger(__name__).error(
|
||||
"%s and %s can only be set to %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET)
|
||||
return None
|
||||
|
||||
def sunrise_automation_listener(now):
|
||||
""" Called when it's time for action. """
|
||||
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
|
||||
action()
|
||||
before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET))
|
||||
after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET))
|
||||
if before_offset is False or after_offset is False:
|
||||
return None
|
||||
|
||||
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
|
||||
if before is None:
|
||||
def before_func():
|
||||
"""Return no point in time."""
|
||||
return None
|
||||
elif before == EVENT_SUNRISE:
|
||||
def before_func():
|
||||
"""Return time before sunrise."""
|
||||
return sun.next_rising(hass) + before_offset
|
||||
else:
|
||||
def before_func():
|
||||
"""Return time before sunset."""
|
||||
return sun.next_setting(hass) + before_offset
|
||||
|
||||
if after is None:
|
||||
def after_func():
|
||||
"""Return no point in time."""
|
||||
return None
|
||||
elif after == EVENT_SUNRISE:
|
||||
def after_func():
|
||||
"""Return time after sunrise."""
|
||||
return sun.next_rising(hass) + after_offset
|
||||
else:
|
||||
def after_func():
|
||||
"""Return time after sunset."""
|
||||
return sun.next_setting(hass) + after_offset
|
||||
|
||||
def time_if():
|
||||
""" Validate time based if-condition """
|
||||
|
||||
now = dt_util.now()
|
||||
before = before_func()
|
||||
after = after_func()
|
||||
|
||||
if before is not None and now > now.replace(hour=before.hour,
|
||||
minute=before.minute):
|
||||
return False
|
||||
|
||||
if after is not None and now < now.replace(hour=after.hour,
|
||||
minute=after.minute):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return time_if
|
||||
|
||||
|
||||
def trigger_sunset(hass, action, offset):
|
||||
""" Trigger action at next sun set. """
|
||||
def next_set():
|
||||
""" Returns next sunrise. """
|
||||
next_time = sun.next_setting_utc(hass) + offset
|
||||
def _parse_offset(raw_offset):
|
||||
if raw_offset is None:
|
||||
return timedelta(0)
|
||||
|
||||
while next_time < dt_util.utcnow():
|
||||
next_time = next_time + timedelta(days=1)
|
||||
negative_offset = False
|
||||
if raw_offset.startswith('-'):
|
||||
negative_offset = True
|
||||
raw_offset = raw_offset[1:]
|
||||
|
||||
return next_time
|
||||
try:
|
||||
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
|
||||
except ValueError:
|
||||
_LOGGER.error('Could not parse offset %s', raw_offset)
|
||||
return False
|
||||
|
||||
def sunset_automation_listener(now):
|
||||
""" Called when it's time for action. """
|
||||
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
|
||||
action()
|
||||
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
|
||||
if negative_offset:
|
||||
offset *= -1
|
||||
|
||||
return offset
|
||||
|
||||
@@ -8,7 +8,6 @@ at https://home-assistant.io/components/automation/#time-trigger
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
|
||||
@@ -32,11 +31,11 @@ def trigger(hass, config, action):
|
||||
_error_time(config[CONF_AFTER], CONF_AFTER)
|
||||
return False
|
||||
hours, minutes, seconds = after.hour, after.minute, after.second
|
||||
elif (CONF_HOURS in config or CONF_MINUTES in config
|
||||
or CONF_SECONDS in config):
|
||||
hours = convert(config.get(CONF_HOURS), int)
|
||||
minutes = convert(config.get(CONF_MINUTES), int)
|
||||
seconds = convert(config.get(CONF_SECONDS), int)
|
||||
elif (CONF_HOURS in config or CONF_MINUTES in config or
|
||||
CONF_SECONDS in config):
|
||||
hours = config.get(CONF_HOURS)
|
||||
minutes = config.get(CONF_MINUTES)
|
||||
seconds = config.get(CONF_SECONDS)
|
||||
else:
|
||||
_LOGGER.error('One of %s, %s, %s OR %s needs to be specified',
|
||||
CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER)
|
||||
@@ -59,7 +58,7 @@ def if_action(hass, config):
|
||||
weekday = config.get(CONF_WEEKDAY)
|
||||
|
||||
if before is None and after is None and weekday is None:
|
||||
logging.getLogger(__name__).error(
|
||||
_LOGGER.error(
|
||||
"Missing if-condition configuration key %s, %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY)
|
||||
return None
|
||||
|
||||
44
homeassistant/components/binary_sensor/apcupsd.py
Normal file
44
homeassistant/components/binary_sensor/apcupsd.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.apcupsd
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides a binary sensor to track online status of a UPS.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.apcupsd/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components import apcupsd
|
||||
|
||||
DEPENDENCIES = [apcupsd.DOMAIN]
|
||||
DEFAULT_NAME = "UPS Online Status"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
""" Instantiate an OnlineStatus binary sensor entity and add it to HA. """
|
||||
add_entities((OnlineStatus(config, apcupsd.DATA),))
|
||||
|
||||
|
||||
class OnlineStatus(BinarySensorDevice):
|
||||
""" Binary sensor to represent UPS online status. """
|
||||
def __init__(self, config, data):
|
||||
self._config = config
|
||||
self._data = data
|
||||
self._state = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the UPS online status sensor. """
|
||||
return self._config.get("name", DEFAULT_NAME)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the UPS is online, else False. """
|
||||
return self._state == apcupsd.VALUE_ONLINE
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Get the status report from APCUPSd (or cache) and set this entity's
|
||||
state.
|
||||
"""
|
||||
self._state = self._data.status[apcupsd.KEY_STATUS]
|
||||
84
homeassistant/components/binary_sensor/command_sensor.py
Normal file
84
homeassistant/components/binary_sensor/command_sensor.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.command_sensor
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure custom shell commands to turn a value
|
||||
into a logical value for a binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.command/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.command_sensor import CommandSensorData
|
||||
from homeassistant.util import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Binary Command Sensor"
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Add the Command Sensor. """
|
||||
|
||||
if config.get('command') is None:
|
||||
_LOGGER.error('Missing required variable: "command"')
|
||||
return False
|
||||
|
||||
data = CommandSensorData(config.get('command'))
|
||||
|
||||
add_devices([CommandBinarySensor(
|
||||
hass,
|
||||
data,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
||||
config.get(CONF_VALUE_TEMPLATE)
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class CommandBinarySensor(BinarySensorDevice):
|
||||
""" Represents a binary sensor that is returning
|
||||
a value of a shell commands. """
|
||||
def __init__(self, hass, data, name, payload_on,
|
||||
payload_off, value_template):
|
||||
self._hass = hass
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._value_template = value_template
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data and updates the state. """
|
||||
self.data.update()
|
||||
value = self.data.value
|
||||
|
||||
if self._value_template is not None:
|
||||
value = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, value, False)
|
||||
if value == self._payload_on:
|
||||
self._state = True
|
||||
elif value == self._payload_off:
|
||||
self._state = False
|
||||
56
homeassistant/components/binary_sensor/nest.py
Normal file
56
homeassistant/components/binary_sensor/nest.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.nest
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Nest Thermostat Binary Sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.nest/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
import homeassistant.components.nest as nest
|
||||
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
BINARY_TYPES = ['fan',
|
||||
'hvac_ac_state',
|
||||
'hvac_aux_heater_state',
|
||||
'hvac_heater_state',
|
||||
'hvac_heat_x2_state',
|
||||
'hvac_heat_x3_state',
|
||||
'hvac_alt_heat_state',
|
||||
'hvac_alt_heat_x2_state',
|
||||
'hvac_emer_heat_state',
|
||||
'online']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Setup Nest binary sensors. """
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
for structure in nest.NEST.structures:
|
||||
for device in structure.devices:
|
||||
for variable in config['monitored_conditions']:
|
||||
if variable in BINARY_TYPES:
|
||||
add_devices([NestBinarySensor(structure,
|
||||
device,
|
||||
variable)])
|
||||
else:
|
||||
logger.error('Nest sensor type: "%s" does not exist',
|
||||
variable)
|
||||
except socket.error:
|
||||
logger.error(
|
||||
"Connection error logging into the nest web service."
|
||||
)
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
""" Represents a Nest binary sensor. """
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
return bool(getattr(self.device, self.variable))
|
||||
@@ -6,12 +6,11 @@ The rest binary sensor will consume responses sent by an exposed REST API.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.util import template, Throttle
|
||||
from homeassistant.util import template
|
||||
from homeassistant.components.sensor.rest import RestData
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -19,61 +18,33 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = 'REST Binary Sensor'
|
||||
DEFAULT_METHOD = 'GET'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the REST binary sensor. """
|
||||
|
||||
use_get = False
|
||||
use_post = False
|
||||
|
||||
""" Setup REST binary sensors. """
|
||||
resource = config.get('resource', None)
|
||||
method = config.get('method', DEFAULT_METHOD)
|
||||
payload = config.get('payload', None)
|
||||
verify_ssl = config.get('verify_ssl', True)
|
||||
|
||||
if method == 'GET':
|
||||
use_get = True
|
||||
elif method == 'POST':
|
||||
use_post = True
|
||||
rest = RestData(method, resource, payload, verify_ssl)
|
||||
rest.update()
|
||||
|
||||
try:
|
||||
if use_get:
|
||||
response = requests.get(resource, timeout=10, verify=verify_ssl)
|
||||
elif use_post:
|
||||
response = requests.post(resource, data=payload, timeout=10,
|
||||
verify=verify_ssl)
|
||||
if not response.ok:
|
||||
_LOGGER.error('Response status is "%s"', response.status_code)
|
||||
return False
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error('Missing resource or schema in configuration. '
|
||||
'Add http:// to your URL.')
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('No route to resource/endpoint: %s',
|
||||
resource)
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch Rest data')
|
||||
return False
|
||||
|
||||
if use_get:
|
||||
rest = RestDataGet(resource, verify_ssl)
|
||||
elif use_post:
|
||||
rest = RestDataPost(resource, payload, verify_ssl)
|
||||
|
||||
add_devices([RestBinarySensor(hass,
|
||||
rest,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
add_devices([RestBinarySensor(
|
||||
hass, rest, config.get('name', DEFAULT_NAME),
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
""" Implements a REST binary sensor. """
|
||||
""" A REST binary sensor. """
|
||||
|
||||
def __init__(self, hass, rest, name, value_template):
|
||||
""" Initialize a REST binary sensor. """
|
||||
self._hass = hass
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
@@ -83,63 +54,20 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the binary sensor. """
|
||||
""" Name of the binary sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
if self.rest.data is False:
|
||||
""" Return if the binary sensor is on. """
|
||||
if self.rest.data is None:
|
||||
return False
|
||||
else:
|
||||
if self._value_template is not None:
|
||||
self.rest.data = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
return bool(int(self.rest.data))
|
||||
|
||||
if self._value_template is not None:
|
||||
self.rest.data = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
return bool(int(self.rest.data))
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from REST API and updates the state. """
|
||||
""" Get the latest data from REST API and updates the state. """
|
||||
self.rest.update()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataGet(object):
|
||||
""" Class for handling the data retrieval with GET method. """
|
||||
|
||||
def __init__(self, resource, verify_ssl):
|
||||
self._resource = resource
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = False
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with GET method. """
|
||||
try:
|
||||
response = requests.get(self._resource, timeout=10,
|
||||
verify=self._verify_ssl)
|
||||
self.data = response.text
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
|
||||
self.data = False
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataPost(object):
|
||||
""" Class for handling the data retrieval with POST method. """
|
||||
|
||||
def __init__(self, resource, payload, verify_ssl):
|
||||
self._resource = resource
|
||||
self._payload = payload
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = False
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with POST method. """
|
||||
try:
|
||||
response = requests.post(self._resource, data=self._payload,
|
||||
timeout=10, verify=self._verify_ssl)
|
||||
self.data = response.text
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
|
||||
self.data = False
|
||||
|
||||
73
homeassistant/components/binary_sensor/rpi_gpio.py
Normal file
73
homeassistant/components/binary_sensor/rpi_gpio.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.rpi_gpio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure a binary sensor using RPi GPIO.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rpi_gpio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import (DEVICE_DEFAULT_NAME)
|
||||
|
||||
DEFAULT_PULL_MODE = "UP"
|
||||
DEFAULT_BOUNCETIME = 50
|
||||
DEFAULT_INVERT_LOGIC = False
|
||||
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Raspberry PI GPIO devices. """
|
||||
|
||||
pull_mode = config.get('pull_mode', DEFAULT_PULL_MODE)
|
||||
bouncetime = config.get('bouncetime', DEFAULT_BOUNCETIME)
|
||||
invert_logic = config.get('invert_logic', DEFAULT_INVERT_LOGIC)
|
||||
|
||||
binary_sensors = []
|
||||
ports = config.get('ports')
|
||||
for port_num, port_name in ports.items():
|
||||
binary_sensors.append(RPiGPIOBinarySensor(
|
||||
port_name, port_num, pull_mode, bouncetime, invert_logic))
|
||||
add_devices(binary_sensors)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
""" Represents a binary sensor that uses Raspberry Pi GPIO. """
|
||||
def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
|
||||
# pylint: disable=no-member
|
||||
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._port = port
|
||||
self._pull_mode = pull_mode
|
||||
self._bouncetime = bouncetime
|
||||
self._invert_logic = invert_logic
|
||||
|
||||
rpi_gpio.setup_input(self._port, self._pull_mode)
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
|
||||
def read_gpio(port):
|
||||
""" Reads state from GPIO. """
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
self.update_ha_state()
|
||||
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" Returns the state of the entity. """
|
||||
return self._state != self._invert_logic
|
||||
29
homeassistant/components/binary_sensor/zigbee.py
Normal file
29
homeassistant/components/binary_sensor/zigbee.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.zigbee
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Contains functionality to use a ZigBee device as a binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.zigbee/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.zigbee import (
|
||||
ZigBeeDigitalIn, ZigBeeDigitalInConfig)
|
||||
|
||||
|
||||
DEPENDENCIES = ["zigbee"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
""" Create and add an entity based on the configuration. """
|
||||
add_entities([
|
||||
ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))
|
||||
])
|
||||
|
||||
|
||||
class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice):
|
||||
"""
|
||||
Use multiple inheritance to turn a ZigBeeDigitalIn into a
|
||||
BinarySensorDevice.
|
||||
"""
|
||||
pass
|
||||
77
homeassistant/components/bloomsky.py
Normal file
77
homeassistant/components/bloomsky.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
homeassistant.components.bloomsky
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for BloomSky weather station.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/bloomsky/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
|
||||
DOMAIN = "bloomsky"
|
||||
BLOOMSKY = None
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The BloomSky only updates every 5-8 minutes as per the API spec so there's
|
||||
# no point in polling the API more frequently
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument,too-few-public-methods
|
||||
def setup(hass, config):
|
||||
""" Setup BloomSky component. """
|
||||
if not validate_config(
|
||||
config,
|
||||
{DOMAIN: [CONF_API_KEY]},
|
||||
_LOGGER):
|
||||
return False
|
||||
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
|
||||
global BLOOMSKY
|
||||
try:
|
||||
BLOOMSKY = BloomSky(api_key)
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BloomSky(object):
|
||||
""" Handle all communication with the BloomSky API. """
|
||||
|
||||
# API documentation at http://weatherlution.com/bloomsky-api/
|
||||
|
||||
API_URL = "https://api.bloomsky.com/api/skydata"
|
||||
|
||||
def __init__(self, api_key):
|
||||
self._api_key = api_key
|
||||
self.devices = {}
|
||||
_LOGGER.debug("Initial bloomsky device load...")
|
||||
self.refresh_devices()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def refresh_devices(self):
|
||||
"""
|
||||
Uses the API to retreive a list of devices associated with an
|
||||
account along with all the sensors on the device.
|
||||
"""
|
||||
_LOGGER.debug("Fetching bloomsky update")
|
||||
response = requests.get(self.API_URL,
|
||||
headers={"Authorization": self._api_key},
|
||||
timeout=10)
|
||||
if response.status_code == 401:
|
||||
raise RuntimeError("Invalid API_KEY")
|
||||
elif response.status_code != 200:
|
||||
_LOGGER.error("Invalid HTTP response: %s", response.status_code)
|
||||
return
|
||||
# create dictionary keyed off of the device unique id
|
||||
self.devices.update({
|
||||
device["DeviceID"]: device for device in response.json()
|
||||
})
|
||||
@@ -33,8 +33,6 @@ SWITCH_ACTION_SNAPSHOT = 'snapshot'
|
||||
|
||||
SERVICE_CAMERA = 'camera_service'
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
|
||||
DEFAULT_RECORDING_SECONDS = 30
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
@@ -46,6 +44,7 @@ DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
|
||||
REC_DIR_PREFIX = 'recording-'
|
||||
REC_IMG_PREFIX = 'recording_image-'
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
|
||||
@@ -121,33 +120,7 @@ def setup(hass, config):
|
||||
try:
|
||||
camera.is_streaming = True
|
||||
camera.update_ha_state()
|
||||
|
||||
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes(
|
||||
'Content-type: multipart/x-mixed-replace; \
|
||||
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
# MJPEG_START_HEADER.format()
|
||||
|
||||
while True:
|
||||
img_bytes = camera.camera_image()
|
||||
if img_bytes is None:
|
||||
continue
|
||||
headers_str = '\r\n'.join((
|
||||
'Content-length: {}'.format(len(img_bytes)),
|
||||
'Content-type: image/jpeg',
|
||||
)) + '\r\n\r\n'
|
||||
|
||||
handler.request.sendall(
|
||||
bytes(headers_str, 'utf-8') +
|
||||
img_bytes +
|
||||
bytes('\r\n', 'utf-8'))
|
||||
|
||||
handler.request.sendall(
|
||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
time.sleep(0.5)
|
||||
camera.mjpeg_stream(handler)
|
||||
|
||||
except (requests.RequestException, IOError):
|
||||
camera.is_streaming = False
|
||||
@@ -190,6 +163,34 @@ class Camera(Entity):
|
||||
""" Return bytes of camera image. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
""" Generate an HTTP MJPEG stream from camera images. """
|
||||
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes(
|
||||
'Content-type: multipart/x-mixed-replace; \
|
||||
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
# MJPEG_START_HEADER.format()
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
if img_bytes is None:
|
||||
continue
|
||||
headers_str = '\r\n'.join((
|
||||
'Content-length: {}'.format(len(img_bytes)),
|
||||
'Content-type: image/jpeg',
|
||||
)) + '\r\n\r\n'
|
||||
|
||||
handler.request.sendall(
|
||||
bytes(headers_str, 'utf-8') +
|
||||
img_bytes +
|
||||
bytes('\r\n', 'utf-8'))
|
||||
|
||||
handler.request.sendall(
|
||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the entity. """
|
||||
|
||||
60
homeassistant/components/camera/bloomsky.py
Normal file
60
homeassistant/components/camera/bloomsky.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
homeassistant.components.camera.bloomsky
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for a camera of a BloomSky weather station.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.bloomsky/
|
||||
"""
|
||||
import logging
|
||||
import requests
|
||||
import homeassistant.components.bloomsky as bloomsky
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
DEPENDENCIES = ["bloomsky"]
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" set up access to BloomSky cameras """
|
||||
for device in bloomsky.BLOOMSKY.devices.values():
|
||||
add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
|
||||
|
||||
|
||||
class BloomSkyCamera(Camera):
|
||||
""" Represents the images published from the BloomSky's camera. """
|
||||
|
||||
def __init__(self, bs, device):
|
||||
""" set up for access to the BloomSky camera images """
|
||||
super(BloomSkyCamera, self).__init__()
|
||||
self._name = device["DeviceName"]
|
||||
self._id = device["DeviceID"]
|
||||
self._bloomsky = bs
|
||||
self._url = ""
|
||||
self._last_url = ""
|
||||
# _last_image will store images as they are downloaded so that the
|
||||
# frequent updates in home-assistant don't keep poking the server
|
||||
# to download the same image over and over
|
||||
self._last_image = ""
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
def camera_image(self):
|
||||
""" Update the camera's image if it has changed. """
|
||||
try:
|
||||
self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"]
|
||||
self._bloomsky.refresh_devices()
|
||||
# if the url hasn't changed then the image hasn't changed
|
||||
if self._url != self._last_url:
|
||||
response = requests.get(self._url, timeout=10)
|
||||
self._last_url = self._url
|
||||
self._last_image = response.content
|
||||
except requests.exceptions.RequestException as error:
|
||||
self._logger.error("Error getting bloomsky image: %s", error)
|
||||
return None
|
||||
|
||||
return self._last_image
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of this BloomSky device. """
|
||||
return self._name
|
||||
@@ -14,6 +14,9 @@ from requests.auth import HTTPBasicAuth
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.const import HTTP_OK
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,6 +44,17 @@ class MjpegCamera(Camera):
|
||||
self._password = device_info.get('password')
|
||||
self._mjpeg_url = device_info['mjpeg_url']
|
||||
|
||||
def camera_stream(self):
|
||||
""" Return a mjpeg stream image response directly from the camera. """
|
||||
if self._username and self._password:
|
||||
return requests.get(self._mjpeg_url,
|
||||
auth=HTTPBasicAuth(self._username,
|
||||
self._password),
|
||||
stream=True)
|
||||
else:
|
||||
return requests.get(self._mjpeg_url,
|
||||
stream=True)
|
||||
|
||||
def camera_image(self):
|
||||
""" Return a still image response from the camera. """
|
||||
|
||||
@@ -55,16 +69,22 @@ class MjpegCamera(Camera):
|
||||
jpg = data[jpg_start:jpg_end + 2]
|
||||
return jpg
|
||||
|
||||
if self._username and self._password:
|
||||
with closing(requests.get(self._mjpeg_url,
|
||||
auth=HTTPBasicAuth(self._username,
|
||||
self._password),
|
||||
stream=True)) as response:
|
||||
return process_response(response)
|
||||
else:
|
||||
with closing(requests.get(self._mjpeg_url,
|
||||
stream=True)) as response:
|
||||
return process_response(response)
|
||||
with closing(self.camera_stream()) as response:
|
||||
return process_response(response)
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
""" 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)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
91
homeassistant/components/camera/uvc.py
Normal file
91
homeassistant/components/camera/uvc.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
homeassistant.components.camera.uvc
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Ubiquiti's UVC cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.uvc/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Discover cameras on a Unifi NVR. """
|
||||
if not validate_config({DOMAIN: config}, {DOMAIN: ['nvr', 'key']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
addr = config.get('nvr')
|
||||
port = int(config.get('port', 7080))
|
||||
key = config.get('key')
|
||||
|
||||
from uvcclient import nvr
|
||||
nvrconn = nvr.UVCRemote(addr, port, key)
|
||||
try:
|
||||
cameras = nvrconn.index()
|
||||
except nvr.NotAuthorized:
|
||||
_LOGGER.error('Authorization failure while connecting to NVR')
|
||||
return False
|
||||
except nvr.NvrError:
|
||||
_LOGGER.error('NVR refuses to talk to me')
|
||||
return False
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to NVR: %s', str(ex))
|
||||
return False
|
||||
|
||||
for camera in cameras:
|
||||
add_devices([UnifiVideoCamera(nvrconn,
|
||||
camera['uuid'],
|
||||
camera['name'])])
|
||||
|
||||
|
||||
class UnifiVideoCamera(Camera):
|
||||
""" A Ubiquiti Unifi Video Camera. """
|
||||
|
||||
def __init__(self, nvr, uuid, name):
|
||||
super(UnifiVideoCamera, self).__init__()
|
||||
self._nvr = nvr
|
||||
self._uuid = uuid
|
||||
self._name = name
|
||||
self.is_streaming = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
return caminfo['recordingSettings']['fullTimeRecordEnabled']
|
||||
|
||||
def camera_image(self):
|
||||
from uvcclient import camera as uvc_camera
|
||||
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
camera = None
|
||||
for addr in [caminfo['host'], caminfo['internalHost']]:
|
||||
try:
|
||||
camera = uvc_camera.UVCCameraClient(addr,
|
||||
caminfo['username'],
|
||||
'ubnt')
|
||||
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
|
||||
dict(name=self._name, addr=addr))
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
if not camera:
|
||||
_LOGGER.error('Unable to login to camera')
|
||||
return None
|
||||
|
||||
camera.login()
|
||||
return camera.get_snapshot()
|
||||
@@ -11,7 +11,7 @@ the user has submitted configuration information.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import generate_entity_id
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.const import EVENT_TIME_CHANGED
|
||||
|
||||
DOMAIN = "configurator"
|
||||
@@ -141,7 +141,7 @@ class Configurator(object):
|
||||
|
||||
state = self.hass.states.get(entity_id)
|
||||
|
||||
new_data = state.attributes
|
||||
new_data = dict(state.attributes)
|
||||
new_data[ATTR_ERRORS] = error
|
||||
|
||||
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
|
||||
|
||||
@@ -21,6 +21,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'binary_sensor',
|
||||
'camera',
|
||||
'device_tracker',
|
||||
'garage_door',
|
||||
'light',
|
||||
'lock',
|
||||
'media_player',
|
||||
@@ -62,10 +63,16 @@ def setup(hass, config):
|
||||
lights = sorted(hass.states.entity_ids('light'))
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
media_players = sorted(hass.states.entity_ids('media_player'))
|
||||
group.setup_group(hass, 'living room', [lights[2], lights[1], switches[0],
|
||||
media_players[1]])
|
||||
group.setup_group(hass, 'bedroom', [lights[0], switches[1],
|
||||
media_players[0]])
|
||||
group.Group(hass, 'living room', [
|
||||
lights[2], lights[1], switches[0], media_players[1],
|
||||
'scene.romantic_lights'])
|
||||
group.Group(hass, 'bedroom', [lights[0], switches[1],
|
||||
media_players[0]])
|
||||
group.Group(hass, 'Rooms', [
|
||||
'group.living_room', 'group.bedroom',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window',
|
||||
], view=True)
|
||||
|
||||
# Setup scripts
|
||||
bootstrap.setup_component(
|
||||
|
||||
@@ -229,7 +229,7 @@ class DeviceTracker(object):
|
||||
""" Initializes group for all tracked devices. """
|
||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||
if dev.track)
|
||||
self.group = group.setup_group(
|
||||
self.group = group.Group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
|
||||
def update_stale(self, now):
|
||||
|
||||
@@ -11,7 +11,6 @@ import logging
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import threading
|
||||
import telnetlib
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers import validate_config
|
||||
@@ -21,6 +20,7 @@ from homeassistant.components.device_tracker import DOMAIN
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DEVICES_REGEX = re.compile(
|
||||
@@ -44,6 +44,7 @@ def get_scanner(hass, config):
|
||||
|
||||
class ArubaDeviceScanner(object):
|
||||
""" This class queries a Aruba Acces Point for connected devices. """
|
||||
|
||||
def __init__(self, config):
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
@@ -93,23 +94,39 @@ class ArubaDeviceScanner(object):
|
||||
|
||||
def get_aruba_data(self):
|
||||
""" Retrieve data from Aruba Access Point and return parsed result. """
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'User: ')
|
||||
telnet.write((self.username + '\r\n').encode('ascii'))
|
||||
telnet.read_until(b'Password: ')
|
||||
telnet.write((self.password + '\r\n').encode('ascii'))
|
||||
telnet.read_until(b'#')
|
||||
telnet.write(('show clients\r\n').encode('ascii'))
|
||||
devices_result = telnet.read_until(b'#').split(b'\r\n')
|
||||
telnet.write('exit\r\n'.encode('ascii'))
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
|
||||
import pexpect
|
||||
connect = "ssh {}@{}"
|
||||
ssh = pexpect.spawn(connect.format(self.username, self.host))
|
||||
query = ssh.expect(['password:', pexpect.TIMEOUT, pexpect.EOF,
|
||||
'continue connecting (yes/no)?',
|
||||
'Host key verification failed.',
|
||||
'Connection refused',
|
||||
'Connection timed out'], timeout=120)
|
||||
if query == 1:
|
||||
_LOGGER.error("Timeout")
|
||||
return
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router," +
|
||||
" is telnet enabled?")
|
||||
elif query == 2:
|
||||
_LOGGER.error("Unexpected response from router")
|
||||
return
|
||||
elif query == 3:
|
||||
ssh.sendline('yes')
|
||||
ssh.expect('password:')
|
||||
elif query == 4:
|
||||
_LOGGER.error("Host key Changed")
|
||||
return
|
||||
elif query == 5:
|
||||
_LOGGER.error("Connection refused by server")
|
||||
return
|
||||
elif query == 6:
|
||||
_LOGGER.error("Connection timed out")
|
||||
return
|
||||
ssh.sendline(self.password)
|
||||
ssh.expect('#')
|
||||
ssh.sendline('show clients')
|
||||
ssh.expect('#')
|
||||
devices_result = ssh.before.split(b'\r\n')
|
||||
ssh.sendline('exit')
|
||||
|
||||
devices = {}
|
||||
for device in devices_result:
|
||||
@@ -119,5 +136,5 @@ class ArubaDeviceScanner(object):
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
'name': match.group('name')
|
||||
}
|
||||
}
|
||||
return devices
|
||||
|
||||
@@ -58,8 +58,8 @@ class AsusWrtDeviceScanner(object):
|
||||
|
||||
def __init__(self, config):
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.username = str(config[CONF_USERNAME])
|
||||
self.password = str(config[CONF_PASSWORD])
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.4.6']
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -55,16 +57,8 @@ class FritzBoxScanner(object):
|
||||
self.password = ''
|
||||
self.success_init = True
|
||||
|
||||
# Try to import the fritzconnection library
|
||||
try:
|
||||
# noinspection PyPackageRequirements,PyUnresolvedReferences
|
||||
import fritzconnection as fc
|
||||
except ImportError:
|
||||
_LOGGER.exception("""Failed to import Python library
|
||||
fritzconnection. Please run
|
||||
<home-assistant>/setup to install it.""")
|
||||
self.success_init = False
|
||||
return
|
||||
# pylint: disable=import-error
|
||||
import fritzconnection as fc
|
||||
|
||||
# Check for user specific configuration
|
||||
if CONF_HOST in config.keys():
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.geofancy
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Geofancy platform for the device tracker.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.geofancy/
|
||||
"""
|
||||
from homeassistant.const import (
|
||||
HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_SEE = 0
|
||||
|
||||
URL_API_GEOFANCY_ENDPOINT = "/api/geofancy"
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up an endpoint for the Geofancy app. """
|
||||
|
||||
# Use a global variable to keep setup_scanner compact when using a callback
|
||||
global _SEE
|
||||
_SEE = see
|
||||
|
||||
# POST would be semantically better, but that currently does not work
|
||||
# since Geofancy 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_GEOFANCY_ENDPOINT, _handle_get_api_geofancy)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_geofancy(handler, path_match, data):
|
||||
""" Geofancy message received. """
|
||||
|
||||
if not isinstance(data, dict):
|
||||
handler.write_json_message(
|
||||
"Error while parsing Geofancy message.",
|
||||
HTTP_INTERNAL_SERVER_ERROR)
|
||||
return
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
handler.write_json_message(
|
||||
"Location not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
if 'device' not in data or 'id' not in data:
|
||||
handler.write_json_message(
|
||||
"Device id or location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
try:
|
||||
gps_coords = (float(data['latitude']), float(data['longitude']))
|
||||
except ValueError:
|
||||
# If invalid latitude / longitude format
|
||||
handler.write_json_message(
|
||||
"Invalid latitude / longitude format.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
# entity id's in Home Assistant must be alphanumerical
|
||||
device_uuid = data['device']
|
||||
device_entity_id = device_uuid.replace('-', '')
|
||||
|
||||
_SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id'])
|
||||
|
||||
handler.write_json_message("Geofancy message processed")
|
||||
104
homeassistant/components/device_tracker/locative.py
Normal file
104
homeassistant/components/device_tracker/locative.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.locative
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Locative platform for the device tracker.
|
||||
|
||||
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.const import (
|
||||
HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up an endpoint for the Locative app. """
|
||||
|
||||
# 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))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_locative(hass, see, handler, path_match, data):
|
||||
""" Locative message received. """
|
||||
|
||||
if not _check_data(handler, data):
|
||||
return
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
|
||||
if direction == 'enter':
|
||||
see(dev_id=device, location_name=location_name)
|
||||
handler.write_text("Setting location to {}".format(location_name))
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = hass.states.get("{}.{}".format(DOMAIN, device))
|
||||
|
||||
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):
|
||||
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
|
||||
@@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pynetgear==0.3']
|
||||
REQUIREMENTS = ['pynetgear==0.3.2']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
|
||||
@@ -8,16 +8,29 @@ https://home-assistant.io/components/device_tracker.owntracks/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import STATE_HOME
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
REGIONS_ENTERED = defaultdict(list)
|
||||
MOBILE_BEACONS_ACTIVE = defaultdict(list)
|
||||
|
||||
BEACON_DEV_ID = 'beacon'
|
||||
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LOCK = threading.Lock()
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a OwnTracksks tracker. """
|
||||
""" Set up an OwnTracks tracker. """
|
||||
|
||||
def owntracks_location_update(topic, payload, qos):
|
||||
""" MQTT message received. """
|
||||
@@ -28,26 +41,143 @@ def setup_scanner(hass, config, see):
|
||||
data = json.loads(payload)
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
logging.getLogger(__name__).error(
|
||||
_LOGGER.error(
|
||||
'Unable to parse payload as JSON: %s', payload)
|
||||
return
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != 'location':
|
||||
return
|
||||
|
||||
parts = topic.split('/')
|
||||
kwargs = {
|
||||
'dev_id': '{}_{}'.format(parts[1], parts[2]),
|
||||
'host_name': parts[1],
|
||||
'gps': (data['lat'], data['lon']),
|
||||
}
|
||||
if 'acc' in data:
|
||||
kwargs['gps_accuracy'] = data['acc']
|
||||
if 'batt' in data:
|
||||
kwargs['battery'] = data['batt']
|
||||
dev_id, kwargs = _parse_see_args(topic, data)
|
||||
|
||||
see(**kwargs)
|
||||
# Block updates if we're in a region
|
||||
with LOCK:
|
||||
if REGIONS_ENTERED[dev_id]:
|
||||
_LOGGER.debug(
|
||||
"location update ignored - inside region %s",
|
||||
REGIONS_ENTERED[-1])
|
||||
return
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
|
||||
def owntracks_event_update(topic, payload, qos):
|
||||
# pylint: disable=too-many-branches
|
||||
""" MQTT event (geofences) received. """
|
||||
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
_LOGGER.error(
|
||||
'Unable to parse payload as JSON: %s', payload)
|
||||
return
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != 'transition':
|
||||
return
|
||||
|
||||
# OwnTracks uses - at the start of a beacon zone
|
||||
# to switch on 'hold mode' - ignore this
|
||||
location = data['desc'].lstrip("-")
|
||||
if location.lower() == 'home':
|
||||
location = STATE_HOME
|
||||
|
||||
dev_id, kwargs = _parse_see_args(topic, data)
|
||||
|
||||
if data['event'] == 'enter':
|
||||
zone = hass.states.get("zone.{}".format(location))
|
||||
with LOCK:
|
||||
if zone is None:
|
||||
if data['t'] == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
MOBILE_BEACONS_ACTIVE[dev_id].append(location)
|
||||
else:
|
||||
# Normal region
|
||||
if not zone.attributes.get('passive'):
|
||||
kwargs['location_name'] = location
|
||||
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location not in regions:
|
||||
regions.append(location)
|
||||
_LOGGER.info("Enter region %s", location)
|
||||
_set_gps_from_zone(kwargs, zone)
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
|
||||
elif data['event'] == 'leave':
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location in regions:
|
||||
regions.remove(location)
|
||||
new_region = regions[-1] if regions else None
|
||||
|
||||
if new_region:
|
||||
# Exit to previous region
|
||||
zone = hass.states.get("zone.{}".format(new_region))
|
||||
if not zone.attributes.get('passive'):
|
||||
kwargs['location_name'] = new_region
|
||||
_set_gps_from_zone(kwargs, zone)
|
||||
_LOGGER.info("Exit from to %s", new_region)
|
||||
|
||||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Misformatted mqtt msgs, _type=transition, event=%s',
|
||||
data['event'])
|
||||
return
|
||||
|
||||
def see_beacons(dev_id, kwargs_param):
|
||||
""" Set active beacons to the current location """
|
||||
|
||||
kwargs = kwargs_param.copy()
|
||||
for beacon in MOBILE_BEACONS_ACTIVE[dev_id]:
|
||||
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
|
||||
kwargs['host_name'] = beacon
|
||||
see(**kwargs)
|
||||
|
||||
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
||||
|
||||
mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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])
|
||||
host_name = parts[1]
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
'host_name': host_name,
|
||||
'gps': (data['lat'], data['lon'])
|
||||
}
|
||||
if 'acc' in data:
|
||||
kwargs['gps_accuracy'] = data['acc']
|
||||
if 'batt' in data:
|
||||
kwargs['battery'] = data['batt']
|
||||
return dev_id, kwargs
|
||||
|
||||
|
||||
def _set_gps_from_zone(kwargs, zone):
|
||||
""" Set the see parameters from the zone parameters """
|
||||
|
||||
if zone is not None:
|
||||
kwargs['gps'] = (
|
||||
zone.attributes['latitude'],
|
||||
zone.attributes['longitude'])
|
||||
kwargs['gps_accuracy'] = zone.attributes['radius']
|
||||
return kwargs
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Describes the format for available device tracker services
|
||||
|
||||
see:
|
||||
description: Control tracked device
|
||||
|
||||
fields:
|
||||
mac:
|
||||
description: MAC address of device
|
||||
example: 'FF:FF:FF:FF:FF:FF'
|
||||
|
||||
dev_id:
|
||||
description: Id of device (find id in known_devices.yaml)
|
||||
example: 'phonedave'
|
||||
|
||||
host_name:
|
||||
description: Hostname of device
|
||||
example: 'Dave'
|
||||
|
||||
location_name:
|
||||
description: Name of location where device is located (not_home is away)
|
||||
example: 'home'
|
||||
|
||||
gps:
|
||||
description: GPS coordinates where device is located (latitude, longitude)
|
||||
example: '[51.509802, -0.086692]'
|
||||
|
||||
gps_accuracy:
|
||||
description: Accuracy of GPS coordinates
|
||||
example: '80'
|
||||
|
||||
battery:
|
||||
description: Battery level of device
|
||||
example: '100'
|
||||
|
||||
@@ -105,8 +105,7 @@ class SnmpScanner(object):
|
||||
return
|
||||
if errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
errindex and restable[-1][int(errindex)-1]
|
||||
or '?')
|
||||
errindex and restable[-1][int(errindex)-1] or '?')
|
||||
return
|
||||
|
||||
for resrow in restable:
|
||||
|
||||
@@ -242,8 +242,8 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/;stok={}/admin/wireless?form=statistics' \
|
||||
.format(self.host, self.stok)
|
||||
url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?'
|
||||
'form=statistics').format(self.host, self.stok)
|
||||
referer = 'http://{}/webpages/index.html'.format(self.host)
|
||||
|
||||
response = requests.post(url,
|
||||
|
||||
@@ -11,6 +11,8 @@ import logging
|
||||
import re
|
||||
import threading
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import sanitize_filename
|
||||
|
||||
@@ -30,14 +32,6 @@ def setup(hass, config):
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
logger.exception(("Failed to import requests. "
|
||||
"Did you maybe not execute 'pip install requests'?"))
|
||||
|
||||
return False
|
||||
|
||||
if not validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger):
|
||||
return False
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FRONTEND_URLS = [
|
||||
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent', '/devInfo', '/devTemplate', '/states']
|
||||
'/devEvent', '/devInfo', '/devTemplate',
|
||||
re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)'),
|
||||
]
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by update_mdi script """
|
||||
VERSION = "7d76081c37634d36af21f5cc1ca79408"
|
||||
VERSION = "a1a203680639ff1abcc7b68cdb29c57a"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "be08c5a3ce12040bbdba2db83cb1a568"
|
||||
VERSION = "833d09737fec24f9219efae87c5bfd2a"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
|
||||
!*** ./src/service-worker/index.js ***!
|
||||
\*************************************/
|
||||
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){var r=fetch(e.request).then(function(e){return t.put(s,e.clone()),e});return n||r})}))})}]);
|
||||
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}]);
|
||||
//# sourceMappingURL=service_worker.js.map
|
||||
108
homeassistant/components/garage_door/__init__.py
Normal file
108
homeassistant/components/garage_door/__init__.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
homeassistant.components.garage_door
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with garage doors that can be controlled remotely.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
at https://home-assistant.io/components/garage_door/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.components import (group, wink)
|
||||
|
||||
DOMAIN = 'garage_door'
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
GROUP_NAME_ALL_GARAGE_DOORS = 'all garage doors'
|
||||
ENTITY_ID_ALL_GARAGE_DOORS = group.ENTITY_ID_FORMAT.format('all_garage_doors')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_GARAGE_DOORS: 'wink'
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_closed(hass, entity_id=None):
|
||||
""" Returns if the garage door is closed based on the statemachine. """
|
||||
entity_id = entity_id or ENTITY_ID_ALL_GARAGE_DOORS
|
||||
return hass.states.is_state(entity_id, STATE_CLOSED)
|
||||
|
||||
|
||||
def close_door(hass, entity_id=None):
|
||||
""" Closes all or specified garage door. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_CLOSE, data)
|
||||
|
||||
|
||||
def open_door(hass, entity_id=None):
|
||||
""" Open all or specified garage door. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_OPEN, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for garage door. """
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
||||
GROUP_NAME_ALL_GARAGE_DOORS)
|
||||
component.setup(config)
|
||||
|
||||
def handle_garage_door_service(service):
|
||||
""" Handles calls to the garage door services. """
|
||||
target_locks = component.extract_from_service(service)
|
||||
|
||||
for item in target_locks:
|
||||
if service.service == SERVICE_CLOSE:
|
||||
item.close_door()
|
||||
else:
|
||||
item.open_door()
|
||||
|
||||
if item.should_poll:
|
||||
item.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_OPEN, handle_garage_door_service,
|
||||
descriptions.get(SERVICE_OPEN))
|
||||
hass.services.register(DOMAIN, SERVICE_CLOSE, handle_garage_door_service,
|
||||
descriptions.get(SERVICE_CLOSE))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GarageDoorDevice(Entity):
|
||||
""" Represents a garage door. """
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
""" Is the garage door closed or opened. """
|
||||
return None
|
||||
|
||||
def close_door(self):
|
||||
""" Closes the garage door. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def open_door(self):
|
||||
""" Opens the garage door. """
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" State of the garage door. """
|
||||
closed = self.is_closed
|
||||
if closed is None:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_CLOSED if closed else STATE_OPEN
|
||||
48
homeassistant/components/garage_door/demo.py
Normal file
48
homeassistant/components/garage_door/demo.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
homeassistant.components.garage_door.demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Demo platform that has two fake garage doors.
|
||||
"""
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
from homeassistant.const import STATE_CLOSED, STATE_OPEN
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Find and return demo garage doors. """
|
||||
add_devices_callback([
|
||||
DemoGarageDoor('Left Garage Door', STATE_CLOSED),
|
||||
DemoGarageDoor('Right Garage Door', STATE_OPEN)
|
||||
])
|
||||
|
||||
|
||||
class DemoGarageDoor(GarageDoorDevice):
|
||||
""" Provides a demo garage door. """
|
||||
def __init__(self, name, state):
|
||||
self._name = name
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a demo garage door. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
""" True if device is closed. """
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
def close_door(self, **kwargs):
|
||||
""" Close the device. """
|
||||
self._state = STATE_CLOSED
|
||||
self.update_ha_state()
|
||||
|
||||
def open_door(self, **kwargs):
|
||||
""" Open the device. """
|
||||
self._state = STATE_OPEN
|
||||
self.update_ha_state()
|
||||
0
homeassistant/components/garage_door/services.yaml
Normal file
0
homeassistant/components/garage_door/services.yaml
Normal file
67
homeassistant/components/garage_door/wink.py
Normal file
67
homeassistant/components/garage_door/wink.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
homeassistant.components.garage_door.wink
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Wink garage doors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/garage_door.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.6.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Wink platform. """
|
||||
import pywink
|
||||
|
||||
if discovery_info is None:
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if token is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing wink access_token. "
|
||||
"Get one at https://winkbearertoken.appspot.com/")
|
||||
return
|
||||
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
add_devices(WinkGarageDoorDevice(door) for door in
|
||||
pywink.get_garage_doors())
|
||||
|
||||
|
||||
class WinkGarageDoorDevice(GarageDoorDevice):
|
||||
""" Represents a Wink garage door. """
|
||||
|
||||
def __init__(self, wink):
|
||||
self.wink = wink
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this wink garage door """
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the garage door if any. """
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
""" Update the state of the garage door. """
|
||||
self.wink.update_state()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
""" True if device is closed. """
|
||||
return self.wink.state() == 0
|
||||
|
||||
def close_door(self):
|
||||
""" Close the device. """
|
||||
self.wink.set_state(0)
|
||||
|
||||
def open_door(self):
|
||||
""" Open the device. """
|
||||
self.wink.set_state(1)
|
||||
122
homeassistant/components/graphite.py
Normal file
122
homeassistant/components/graphite.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
homeassistant.components.graphite
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component that records all events and state changes and feeds the data to
|
||||
a graphite installation.
|
||||
|
||||
Example configuration:
|
||||
|
||||
graphite:
|
||||
host: foobar
|
||||
port: 2003
|
||||
prefix: ha
|
||||
|
||||
All config elements are optional, and assumed to be on localhost at the
|
||||
default port if not specified. Prefix is the metric prefix in graphite,
|
||||
and defaults to 'ha'.
|
||||
"""
|
||||
import logging
|
||||
import queue
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_STATE_CHANGED,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import state
|
||||
|
||||
DOMAIN = "graphite"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup graphite feeder. """
|
||||
graphite_config = config.get('graphite', {})
|
||||
host = graphite_config.get('host', 'localhost')
|
||||
prefix = graphite_config.get('prefix', 'ha')
|
||||
try:
|
||||
port = int(graphite_config.get('port', 2003))
|
||||
except ValueError:
|
||||
_LOGGER.error('Invalid port specified')
|
||||
return False
|
||||
|
||||
GraphiteFeeder(hass, host, port, prefix)
|
||||
return True
|
||||
|
||||
|
||||
class GraphiteFeeder(threading.Thread):
|
||||
""" Feeds data to graphite. """
|
||||
def __init__(self, hass, host, port, prefix):
|
||||
super(GraphiteFeeder, self).__init__(daemon=True)
|
||||
self._hass = hass
|
||||
self._host = host
|
||||
self._port = port
|
||||
# rstrip any trailing dots in case they think they
|
||||
# need it
|
||||
self._prefix = prefix.rstrip('.')
|
||||
self._queue = queue.Queue()
|
||||
self._quit_object = object()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||
self.start_listen)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
self.shutdown)
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, self.event_listener)
|
||||
|
||||
def start_listen(self, event):
|
||||
""" Start event-processing thread. """
|
||||
self.start()
|
||||
|
||||
def shutdown(self, event):
|
||||
""" Tell the thread that we are done.
|
||||
|
||||
This does not block because there is nothing to
|
||||
clean up (and no penalty for killing in-process
|
||||
connections to graphite.
|
||||
"""
|
||||
self._queue.put(self._quit_object)
|
||||
|
||||
def event_listener(self, event):
|
||||
""" Queue an event for processing. """
|
||||
self._queue.put(event)
|
||||
|
||||
def _send_to_graphite(self, data):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
sock.connect((self._host, self._port))
|
||||
sock.sendall(data.encode('ascii'))
|
||||
sock.send('\n'.encode('ascii'))
|
||||
sock.close()
|
||||
|
||||
def _report_attributes(self, entity_id, new_state):
|
||||
now = time.time()
|
||||
things = dict(new_state.attributes)
|
||||
try:
|
||||
things['state'] = state.state_as_number(new_state)
|
||||
except ValueError:
|
||||
pass
|
||||
lines = ['%s.%s.%s %f %i' % (self._prefix,
|
||||
entity_id, key.replace(' ', '_'),
|
||||
value, now)
|
||||
for key, value in things.items()
|
||||
if isinstance(value, (float, int))]
|
||||
if not lines:
|
||||
return
|
||||
_LOGGER.debug('Sending to graphite: %s', lines)
|
||||
try:
|
||||
self._send_to_graphite('\n'.join(lines))
|
||||
except socket.error:
|
||||
_LOGGER.exception('Failed to send data to graphite')
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
event = self._queue.get()
|
||||
if event == self._quit_object:
|
||||
self._queue.task_done()
|
||||
return
|
||||
elif (event.event_type == EVENT_STATE_CHANGED and
|
||||
'new_state' in event.data):
|
||||
self._report_attributes(event.data['entity_id'],
|
||||
event.data['new_state'])
|
||||
self._queue.task_done()
|
||||
@@ -7,20 +7,24 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/group/
|
||||
"""
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.helpers import generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers.entity import (
|
||||
Entity, split_entity_id, generate_entity_id)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
|
||||
STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED,
|
||||
STATE_UNKNOWN)
|
||||
STATE_UNKNOWN, CONF_NAME, CONF_ICON)
|
||||
|
||||
DOMAIN = "group"
|
||||
DOMAIN = 'group'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
ATTR_AUTO = "auto"
|
||||
CONF_ENTITIES = 'entities'
|
||||
CONF_VIEW = 'view'
|
||||
|
||||
ATTR_AUTO = 'auto'
|
||||
ATTR_ORDER = 'order'
|
||||
ATTR_VIEW = 'view'
|
||||
|
||||
# List of ON/OFF state tuples for groupable states
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
|
||||
@@ -62,12 +66,12 @@ def expand_entity_ids(hass, entity_ids):
|
||||
|
||||
try:
|
||||
# If entity_id points at a group, expand it
|
||||
domain, _ = util.split_entity_id(entity_id)
|
||||
domain, _ = split_entity_id(entity_id)
|
||||
|
||||
if domain == DOMAIN:
|
||||
found_ids.extend(
|
||||
ent_id for ent_id
|
||||
in get_entity_ids(hass, entity_id)
|
||||
in expand_entity_ids(hass, get_entity_ids(hass, entity_id))
|
||||
if ent_id not in found_ids)
|
||||
|
||||
else:
|
||||
@@ -75,7 +79,7 @@ def expand_entity_ids(hass, entity_ids):
|
||||
found_ids.append(entity_id)
|
||||
|
||||
except AttributeError:
|
||||
# Raised by util.split_entity_id if entity_id is not a string
|
||||
# Raised by split_entity_id if entity_id is not a string
|
||||
pass
|
||||
|
||||
return found_ids
|
||||
@@ -104,10 +108,20 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up all groups found definded in the configuration. """
|
||||
for name, entity_ids in config.get(DOMAIN, {}).items():
|
||||
for object_id, conf in config.get(DOMAIN, {}).items():
|
||||
if not isinstance(conf, dict):
|
||||
conf = {CONF_ENTITIES: conf}
|
||||
|
||||
name = conf.get(CONF_NAME, object_id)
|
||||
entity_ids = conf.get(CONF_ENTITIES)
|
||||
icon = conf.get(CONF_ICON)
|
||||
view = conf.get(CONF_VIEW)
|
||||
|
||||
if isinstance(entity_ids, str):
|
||||
entity_ids = [ent.strip() for ent in entity_ids.split(",")]
|
||||
setup_group(hass, name, entity_ids)
|
||||
|
||||
Group(hass, name, entity_ids, icon=icon, view=view,
|
||||
object_id=object_id)
|
||||
|
||||
return True
|
||||
|
||||
@@ -115,14 +129,19 @@ def setup(hass, config):
|
||||
class Group(Entity):
|
||||
""" Tracks a group of entity ids. """
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
|
||||
def __init__(self, hass, name, entity_ids=None, user_defined=True):
|
||||
def __init__(self, hass, name, entity_ids=None, user_defined=True,
|
||||
icon=None, view=False, object_id=None):
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
self.user_defined = user_defined
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass)
|
||||
self._order = len(hass.states.entity_ids(DOMAIN))
|
||||
self._user_defined = user_defined
|
||||
self._icon = icon
|
||||
self._view = view
|
||||
self.entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, object_id or name, hass=hass)
|
||||
self.tracking = []
|
||||
self.group_on = None
|
||||
self.group_off = None
|
||||
@@ -144,12 +163,25 @@ class Group(Entity):
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
return not self._user_defined or self._view
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
return {
|
||||
data = {
|
||||
ATTR_ENTITY_ID: self.tracking,
|
||||
ATTR_AUTO: not self.user_defined,
|
||||
ATTR_ORDER: self._order,
|
||||
}
|
||||
if not self._user_defined:
|
||||
data[ATTR_AUTO] = True
|
||||
if self._view:
|
||||
data[ATTR_VIEW] = True
|
||||
return data
|
||||
|
||||
def update_tracked_entity_ids(self, entity_ids):
|
||||
""" Update the tracked entity IDs. """
|
||||
@@ -220,10 +252,3 @@ class Group(Entity):
|
||||
for ent_id in self.tracking
|
||||
if tr_state.entity_id != ent_id):
|
||||
self._state = group_off
|
||||
|
||||
|
||||
def setup_group(hass, name, entity_ids, user_defined=True):
|
||||
""" Sets up a group state that is the combined state of
|
||||
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
|
||||
|
||||
return Group(hass, name, entity_ids, user_defined)
|
||||
|
||||
@@ -18,6 +18,8 @@ from homeassistant.const import HTTP_BAD_REQUEST
|
||||
DOMAIN = 'history'
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
|
||||
SIGNIFICANT_DOMAINS = ('thermostat',)
|
||||
|
||||
URL_HISTORY_PERIOD = re.compile(
|
||||
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
|
||||
|
||||
@@ -35,6 +37,37 @@ def last_5_states(entity_id):
|
||||
return recorder.query_states(query, (entity_id, ))
|
||||
|
||||
|
||||
def get_significant_states(start_time, end_time=None, entity_id=None):
|
||||
"""Return states changes during UTC period start_time - end_time.
|
||||
|
||||
Significant states are all states where there is a state change,
|
||||
as well as all states from certain domains (for instance
|
||||
thermostat so that we get current temperature in our graphs).
|
||||
|
||||
"""
|
||||
where = """
|
||||
(domain in ({}) or last_changed=last_updated)
|
||||
AND last_updated > ?
|
||||
""".format(",".join(["'%s'" % x for x in SIGNIFICANT_DOMAINS]))
|
||||
|
||||
data = [start_time]
|
||||
|
||||
if end_time is not None:
|
||||
where += "AND last_updated < ? "
|
||||
data.append(end_time)
|
||||
|
||||
if entity_id is not None:
|
||||
where += "AND entity_id = ? "
|
||||
data.append(entity_id.lower())
|
||||
|
||||
query = ("SELECT * FROM states WHERE {} "
|
||||
"ORDER BY entity_id, last_updated ASC").format(where)
|
||||
|
||||
states = recorder.query_states(query, data)
|
||||
|
||||
return states_to_json(states, start_time, entity_id)
|
||||
|
||||
|
||||
def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
||||
"""
|
||||
Return states changes during UTC period start_time - end_time.
|
||||
@@ -55,20 +88,7 @@ def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
||||
|
||||
states = recorder.query_states(query, data)
|
||||
|
||||
result = defaultdict(list)
|
||||
|
||||
entity_ids = [entity_id] if entity_id is not None else None
|
||||
|
||||
# Get the states at the start time
|
||||
for state in get_states(start_time, entity_ids):
|
||||
state.last_changed = start_time
|
||||
result[state.entity_id].append(state)
|
||||
|
||||
# Append all changes to it
|
||||
for entity_id, group in groupby(states, lambda state: state.entity_id):
|
||||
result[entity_id].extend(group)
|
||||
|
||||
return result
|
||||
return states_to_json(states, start_time, entity_id)
|
||||
|
||||
|
||||
def get_states(utc_point_in_time, entity_ids=None, run=None):
|
||||
@@ -100,6 +120,33 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
|
||||
return recorder.query_states(query, where_data)
|
||||
|
||||
|
||||
def states_to_json(states, start_time, entity_id):
|
||||
"""Converts SQL results into JSON friendly data structure.
|
||||
|
||||
This takes our state list and turns it into a JSON friendly data
|
||||
structure {'entity_id': [list of states], 'entity_id2': [list of states]}
|
||||
|
||||
We also need to go back and create a synthetic zero data point for
|
||||
each list of states, otherwise our graphs won't start on the Y
|
||||
axis correctly.
|
||||
"""
|
||||
|
||||
result = defaultdict(list)
|
||||
|
||||
entity_ids = [entity_id] if entity_id is not None else None
|
||||
|
||||
# Get the states at the start time
|
||||
for state in get_states(start_time, entity_ids):
|
||||
state.last_changed = start_time
|
||||
state.last_updated = start_time
|
||||
result[state.entity_id].append(state)
|
||||
|
||||
# Append all changes to it
|
||||
for entity_id, group in groupby(states, lambda state: state.entity_id):
|
||||
result[entity_id].extend(group)
|
||||
return result
|
||||
|
||||
|
||||
def get_state(utc_point_in_time, entity_id, run=None):
|
||||
""" Return a state at a specific point in time. """
|
||||
states = get_states(utc_point_in_time, (entity_id,), run)
|
||||
@@ -152,4 +199,4 @@ def _api_history_period(handler, path_match, data):
|
||||
entity_id = data.get('filter_entity_id')
|
||||
|
||||
handler.write_json(
|
||||
state_changes_during_period(start_time, end_time, entity_id).values())
|
||||
get_significant_states(start_time, end_time, entity_id).values())
|
||||
|
||||
@@ -21,7 +21,7 @@ from urllib.parse import urlparse, parse_qs
|
||||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, CONTENT_TYPE_JSON,
|
||||
SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN,
|
||||
HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING,
|
||||
HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH,
|
||||
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED,
|
||||
@@ -112,10 +112,10 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
_LOGGER.info("running http in development mode")
|
||||
|
||||
if ssl_certificate is not None:
|
||||
wrap_kwargs = {'certfile': ssl_certificate}
|
||||
if ssl_key is not None:
|
||||
wrap_kwargs['keyfile'] = ssl_key
|
||||
self.socket = ssl.wrap_socket(self.socket, **wrap_kwargs)
|
||||
context = ssl.create_default_context(
|
||||
purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(ssl_certificate, keyfile=ssl_key)
|
||||
self.socket = context.wrap_socket(self.socket, server_side=True)
|
||||
|
||||
def start(self):
|
||||
""" Starts the HTTP server. """
|
||||
@@ -198,12 +198,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
self.authenticated = (self.server.api_password is None
|
||||
or self.headers.get(HTTP_HEADER_HA_AUTH) ==
|
||||
self.server.api_password
|
||||
or data.get(DATA_API_PASSWORD) ==
|
||||
self.server.api_password
|
||||
or self.verify_session())
|
||||
self.authenticated = (self.server.api_password is None or
|
||||
self.headers.get(HTTP_HEADER_HA_AUTH) ==
|
||||
self.server.api_password or
|
||||
data.get(DATA_API_PASSWORD) ==
|
||||
self.server.api_password or
|
||||
self.verify_session())
|
||||
|
||||
if '_METHOD' in data:
|
||||
method = data.pop('_METHOD')
|
||||
@@ -293,6 +293,17 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
json.dumps(data, indent=4, sort_keys=True,
|
||||
cls=rem.JSONEncoder).encode("UTF-8"))
|
||||
|
||||
def write_text(self, message, status_code=HTTP_OK):
|
||||
""" Helper method to return a text message to the caller. """
|
||||
self.send_response(status_code)
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
|
||||
|
||||
self.set_session_cookie_header()
|
||||
|
||||
self.end_headers()
|
||||
|
||||
self.wfile.write(message.encode("UTF-8"))
|
||||
|
||||
def write_file(self, path, cache_headers=True):
|
||||
""" Returns a file to the user. """
|
||||
try:
|
||||
|
||||
@@ -7,13 +7,10 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/influxdb/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF,
|
||||
STATE_UNLOCKED, STATE_LOCKED, STATE_UNKNOWN)
|
||||
from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
|
||||
STATE_BELOW_HORIZON)
|
||||
from homeassistant.helpers import state as state_helper
|
||||
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_UNKNOWN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,14 +20,18 @@ DEPENDENCIES = []
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 8086
|
||||
DEFAULT_DATABASE = 'home_assistant'
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
|
||||
REQUIREMENTS = ['influxdb==2.10.0']
|
||||
REQUIREMENTS = ['influxdb==2.12.0']
|
||||
|
||||
CONF_HOST = 'host'
|
||||
CONF_PORT = 'port'
|
||||
CONF_DB_NAME = 'database'
|
||||
CONF_USERNAME = 'username'
|
||||
CONF_PASSWORD = 'password'
|
||||
CONF_SSL = 'ssl'
|
||||
CONF_VERIFY_SSL = 'verify_ssl'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@@ -38,7 +39,9 @@ def setup(hass, config):
|
||||
|
||||
from influxdb import InfluxDBClient, exceptions
|
||||
|
||||
if not validate_config(config, {DOMAIN: ['host']}, _LOGGER):
|
||||
if not validate_config(config, {DOMAIN: ['host',
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD]}, _LOGGER):
|
||||
return False
|
||||
|
||||
conf = config[DOMAIN]
|
||||
@@ -48,37 +51,36 @@ def setup(hass, config):
|
||||
database = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE)
|
||||
username = util.convert(conf.get(CONF_USERNAME), str)
|
||||
password = util.convert(conf.get(CONF_PASSWORD), str)
|
||||
ssl = util.convert(conf.get(CONF_SSL), bool, DEFAULT_SSL)
|
||||
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
|
||||
DEFAULT_VERIFY_SSL)
|
||||
|
||||
try:
|
||||
influx = InfluxDBClient(host=host, port=port, username=username,
|
||||
password=password, database=database)
|
||||
databases = [i['name'] for i in influx.get_list_database()]
|
||||
except exceptions.InfluxDBClientError:
|
||||
_LOGGER.error("Database host is not accessible. "
|
||||
"Please check your entries in the configuration file.")
|
||||
return False
|
||||
|
||||
if database not in databases:
|
||||
_LOGGER.error("Database %s doesn't exist", database)
|
||||
password=password, database=database,
|
||||
ssl=ssl, verify_ssl=verify_ssl)
|
||||
influx.query("select * from /.*/ LIMIT 1;")
|
||||
except exceptions.InfluxDBClientError as exc:
|
||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||
"check your entries in the configuration file and that"
|
||||
" the database exists and is READ/WRITE.", exc)
|
||||
return False
|
||||
|
||||
def influx_event_listener(event):
|
||||
""" Listen for new messages on the bus and sends them to Influx. """
|
||||
|
||||
state = event.data.get('new_state')
|
||||
|
||||
if state is None:
|
||||
if state is None or state.state in (STATE_UNKNOWN, ''):
|
||||
return
|
||||
|
||||
if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON):
|
||||
_state = 1
|
||||
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN,
|
||||
STATE_BELOW_HORIZON):
|
||||
_state = 0
|
||||
else:
|
||||
try:
|
||||
_state = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
_state = state.state
|
||||
|
||||
measurement = state.attributes.get('unit_of_measurement', state.domain)
|
||||
measurement = state.attributes.get('unit_of_measurement')
|
||||
if measurement in (None, ''):
|
||||
measurement = state.entity_id
|
||||
|
||||
json_body = [
|
||||
{
|
||||
@@ -97,7 +99,7 @@ def setup(hass, config):
|
||||
try:
|
||||
influx.write_points(json_body)
|
||||
except exceptions.InfluxDBClientError:
|
||||
_LOGGER.exception('Error saving event to InfluxDB')
|
||||
_LOGGER.exception('Error saving event "%s" to InfluxDB', json_body)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener)
|
||||
|
||||
|
||||
126
homeassistant/components/input_boolean.py
Normal file
126
homeassistant/components/input_boolean.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
homeassistant.components.input_boolean
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to keep track of user controlled booleans for within automation.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
at https://home-assistant.io/components/input_boolean/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'input_boolean'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = "name"
|
||||
CONF_INITIAL = "initial"
|
||||
CONF_ICON = "icon"
|
||||
|
||||
|
||||
def is_on(hass, entity_id):
|
||||
"""Test if input_boolean is True."""
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
def turn_on(hass, entity_id):
|
||||
"""Set input_boolean to True."""
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id})
|
||||
|
||||
|
||||
def turn_off(hass, entity_id):
|
||||
"""Set input_boolean to False."""
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Set up input boolean. """
|
||||
if not isinstance(config.get(DOMAIN), dict):
|
||||
_LOGGER.error('Expected %s config to be a dictionary', DOMAIN)
|
||||
return False
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
entities = []
|
||||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
if object_id != slugify(object_id):
|
||||
_LOGGER.warning("Found invalid key for boolean input: %s. "
|
||||
"Use %s instead", object_id, slugify(object_id))
|
||||
continue
|
||||
if not cfg:
|
||||
cfg = {}
|
||||
|
||||
name = cfg.get(CONF_NAME)
|
||||
state = cfg.get(CONF_INITIAL, False)
|
||||
icon = cfg.get(CONF_ICON)
|
||||
|
||||
entities.append(InputBoolean(object_id, name, state, icon))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
|
||||
def toggle_service(service):
|
||||
""" Handle a calls to the input boolean services. """
|
||||
target_inputs = component.extract_from_service(service)
|
||||
|
||||
for input_b in target_inputs:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
input_b.turn_on()
|
||||
else:
|
||||
input_b.turn_off()
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, toggle_service)
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, toggle_service)
|
||||
|
||||
component.add_entities(entities)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class InputBoolean(ToggleEntity):
|
||||
""" Represent a boolean input. """
|
||||
|
||||
def __init__(self, object_id, name, state, icon):
|
||||
""" Initialize a boolean input. """
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""If entitiy should be polled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the boolean input."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to be used for this entity."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if entity is on."""
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
140
homeassistant/components/input_select.py
Normal file
140
homeassistant/components/input_select.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
homeassistant.components.input_select
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to offer a way to select an option from a list.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
at https://home-assistant.io/components/input_select/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'input_select'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = 'name'
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_ICON = 'icon'
|
||||
CONF_OPTIONS = 'options'
|
||||
|
||||
ATTR_OPTION = 'option'
|
||||
ATTR_OPTIONS = 'options'
|
||||
|
||||
SERVICE_SELECT_OPTION = 'select_option'
|
||||
|
||||
|
||||
def select_option(hass, entity_id, option):
|
||||
""" Set input_select to False. """
|
||||
hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_OPTION: option,
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Set up input select. """
|
||||
if not isinstance(config.get(DOMAIN), dict):
|
||||
_LOGGER.error('Expected %s config to be a dictionary', DOMAIN)
|
||||
return False
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
entities = []
|
||||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
if object_id != slugify(object_id):
|
||||
_LOGGER.warning("Found invalid key for boolean input: %s. "
|
||||
"Use %s instead", object_id, slugify(object_id))
|
||||
continue
|
||||
if not cfg:
|
||||
_LOGGER.warning("No configuration specified for %s", object_id)
|
||||
continue
|
||||
|
||||
name = cfg.get(CONF_NAME)
|
||||
options = cfg.get(CONF_OPTIONS)
|
||||
|
||||
if not isinstance(options, list) or len(options) == 0:
|
||||
_LOGGER.warning('Key %s should be a list of options', CONF_OPTIONS)
|
||||
continue
|
||||
|
||||
options = [str(val) for val in options]
|
||||
|
||||
state = cfg.get(CONF_INITIAL)
|
||||
|
||||
if state not in options:
|
||||
state = options[0]
|
||||
|
||||
icon = cfg.get(CONF_ICON)
|
||||
|
||||
entities.append(InputSelect(object_id, name, state, options, icon))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
|
||||
def select_option_service(call):
|
||||
""" Handle a calls to the input select services. """
|
||||
target_inputs = component.extract_from_service(call)
|
||||
|
||||
for input_select in target_inputs:
|
||||
input_select.select_option(call.data.get(ATTR_OPTION))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SELECT_OPTION,
|
||||
select_option_service)
|
||||
|
||||
component.add_entities(entities)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class InputSelect(Entity):
|
||||
""" Represent a select input. """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, object_id, name, state, options, icon):
|
||||
""" Initialize a select input. """
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._current_option = state
|
||||
self._options = options
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" If entity should be polled. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Name of the select input. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
""" Icon to be used for this entity. """
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" State of the component. """
|
||||
return self._current_option
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" State attributes. """
|
||||
return {
|
||||
ATTR_OPTIONS: self._options,
|
||||
}
|
||||
|
||||
def select_option(self, option):
|
||||
""" Select new option. """
|
||||
if option not in self._options:
|
||||
_LOGGER.warning('Invalid option: %s (possible options: %s)',
|
||||
option, ', '.join(self._options))
|
||||
return
|
||||
self._current_option = option
|
||||
self.update_ha_state()
|
||||
93
homeassistant/components/insteon_hub.py
Normal file
93
homeassistant/components/insteon_hub.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
homeassistant.components.insteon_hub
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Insteon Hub.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/insteon_hub/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_API_KEY, ATTR_DISCOVERED,
|
||||
ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED)
|
||||
|
||||
DOMAIN = "insteon_hub"
|
||||
REQUIREMENTS = ['insteon_hub==0.4.5']
|
||||
INSTEON = None
|
||||
DISCOVER_LIGHTS = "insteon_hub.lights"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""
|
||||
Setup Insteon Hub component.
|
||||
This will automatically import associated lights.
|
||||
"""
|
||||
if not validate_config(
|
||||
config,
|
||||
{DOMAIN: [CONF_USERNAME, CONF_PASSWORD, CONF_API_KEY]},
|
||||
_LOGGER):
|
||||
return False
|
||||
|
||||
import insteon
|
||||
|
||||
username = config[DOMAIN][CONF_USERNAME]
|
||||
password = config[DOMAIN][CONF_PASSWORD]
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
|
||||
global INSTEON
|
||||
INSTEON = insteon.Insteon(username, password, api_key)
|
||||
|
||||
if INSTEON is None:
|
||||
_LOGGER.error("Could not connect to Insteon service.")
|
||||
return
|
||||
|
||||
comp_name = 'light'
|
||||
discovery = DISCOVER_LIGHTS
|
||||
component = get_component(comp_name)
|
||||
bootstrap.setup_component(hass, component.DOMAIN, config)
|
||||
hass.bus.fire(
|
||||
EVENT_PLATFORM_DISCOVERED,
|
||||
{ATTR_SERVICE: discovery, ATTR_DISCOVERED: {}})
|
||||
return True
|
||||
|
||||
|
||||
class InsteonToggleDevice(ToggleEntity):
|
||||
""" Abstract Class for an Insteon node. """
|
||||
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self._value = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the node. """
|
||||
return self.node.DeviceName
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this insteon node. """
|
||||
return self.node.DeviceID
|
||||
|
||||
def update(self):
|
||||
""" Update state of the sensor. """
|
||||
resp = self.node.send_command('get_status', wait=True)
|
||||
try:
|
||||
self._value = resp['response']['level']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" Returns boolean response if the node is on. """
|
||||
return self._value != 0
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
self.node.send_command('on')
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
self.node.send_command('off')
|
||||
@@ -37,11 +37,7 @@ def setup(hass, config):
|
||||
Setup ISY994 component.
|
||||
This will automatically import associated lights, switches, and sensors.
|
||||
"""
|
||||
try:
|
||||
import PyISY
|
||||
except ImportError:
|
||||
_LOGGER.error("Error while importing dependency PyISY.")
|
||||
return False
|
||||
import PyISY
|
||||
|
||||
# pylint: disable=global-statement
|
||||
# check for required values in configuration file
|
||||
|
||||
@@ -6,8 +6,6 @@ Provides functionality to emulate keyboard presses on host machine.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/keyboard/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
@@ -50,13 +48,7 @@ def media_prev_track(hass):
|
||||
|
||||
def setup(hass, config):
|
||||
""" Listen for keyboard events. """
|
||||
try:
|
||||
import pykeyboard
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).exception(
|
||||
"Error while importing dependency PyUserInput.")
|
||||
|
||||
return False
|
||||
import pykeyboard
|
||||
|
||||
keyboard = pykeyboard.PyKeyboard()
|
||||
keyboard.special_key_assignment()
|
||||
|
||||
@@ -10,10 +10,12 @@ import logging
|
||||
import os
|
||||
import csv
|
||||
|
||||
from homeassistant.components import group, discovery, wink, isy994, zwave
|
||||
from homeassistant.components import (
|
||||
group, discovery, wink, isy994, zwave, insteon_hub, mysensors)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
import homeassistant.util as util
|
||||
@@ -50,6 +52,7 @@ FLASH_LONG = "long"
|
||||
# Apply an effect to the light, can be EFFECT_COLORLOOP
|
||||
ATTR_EFFECT = "effect"
|
||||
EFFECT_COLORLOOP = "colorloop"
|
||||
EFFECT_RANDOM = "random"
|
||||
EFFECT_WHITE = "white"
|
||||
|
||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||
@@ -57,9 +60,11 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_LIGHTS: 'wink',
|
||||
insteon_hub.DISCOVER_LIGHTS: 'insteon_hub',
|
||||
isy994.DISCOVER_LIGHTS: 'isy994',
|
||||
discovery.SERVICE_HUE: 'hue',
|
||||
zwave.DISCOVER_LIGHTS: 'zwave',
|
||||
mysensors.DISCOVER_LIGHTS: 'mysensors',
|
||||
}
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
@@ -113,6 +118,18 @@ def turn_off(hass, entity_id=None, transition=None):
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
|
||||
def toggle(hass, entity_id=None, transition=None):
|
||||
""" Toggles all or specified light. """
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
(ATTR_ENTITY_ID, entity_id),
|
||||
(ATTR_TRANSITION, transition),
|
||||
] if value is not None
|
||||
}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
|
||||
def setup(hass, config):
|
||||
""" Exposes light control via statemachine and services. """
|
||||
@@ -164,9 +181,15 @@ def setup(hass, config):
|
||||
if transition is not None:
|
||||
params[ATTR_TRANSITION] = transition
|
||||
|
||||
service_fun = None
|
||||
if service.service == SERVICE_TURN_OFF:
|
||||
service_fun = 'turn_off'
|
||||
elif service.service == SERVICE_TOGGLE:
|
||||
service_fun = 'toggle'
|
||||
|
||||
if service_fun:
|
||||
for light in target_lights:
|
||||
light.turn_off(**params)
|
||||
getattr(light, service_fun)(**params)
|
||||
|
||||
for light in target_lights:
|
||||
if light.should_poll:
|
||||
@@ -228,7 +251,8 @@ def setup(hass, config):
|
||||
if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG):
|
||||
params[ATTR_FLASH] = dat[ATTR_FLASH]
|
||||
|
||||
if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE):
|
||||
if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE,
|
||||
EFFECT_RANDOM):
|
||||
params[ATTR_EFFECT] = dat[ATTR_EFFECT]
|
||||
|
||||
for light in target_lights:
|
||||
@@ -247,6 +271,9 @@ def setup(hass, config):
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
|
||||
descriptions.get(SERVICE_TURN_OFF))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_light_service,
|
||||
descriptions.get(SERVICE_TOGGLE))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -274,11 +301,6 @@ class Light(ToggleEntity):
|
||||
""" CT color value in mirads. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
@@ -296,9 +318,4 @@ class Light(ToggleEntity):
|
||||
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
|
||||
data[ATTR_BRIGHTNESS])
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
|
||||
return data
|
||||
|
||||
@@ -10,17 +10,18 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
|
||||
from homeassistant.const import CONF_HOST, CONF_FILENAME, DEVICE_DEFAULT_NAME
|
||||
from homeassistant.components.light import (
|
||||
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP,
|
||||
ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT,
|
||||
ATTR_EFFECT, EFFECT_COLORLOOP, ATTR_RGB_COLOR)
|
||||
ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, ATTR_RGB_COLOR)
|
||||
|
||||
REQUIREMENTS = ['phue==0.8']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
@@ -34,9 +35,9 @@ _CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _find_host_from_config(hass):
|
||||
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
|
||||
""" Attempt to detect host based on existing configuration. """
|
||||
path = hass.config.path(PHUE_CONFIG_FILE)
|
||||
path = hass.config.path(filename)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
@@ -53,13 +54,14 @@ def _find_host_from_config(hass):
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Gets the Hue lights. """
|
||||
filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE)
|
||||
if discovery_info is not None:
|
||||
host = urlparse(discovery_info[1]).hostname
|
||||
else:
|
||||
host = config.get(CONF_HOST, None)
|
||||
|
||||
if host is None:
|
||||
host = _find_host_from_config(hass)
|
||||
host = _find_host_from_config(hass, filename)
|
||||
|
||||
if host is None:
|
||||
_LOGGER.error('No host found in configuration')
|
||||
@@ -69,17 +71,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
if host in _CONFIGURING:
|
||||
return
|
||||
|
||||
setup_bridge(host, hass, add_devices_callback)
|
||||
setup_bridge(host, hass, add_devices_callback, filename)
|
||||
|
||||
|
||||
def setup_bridge(host, hass, add_devices_callback):
|
||||
def setup_bridge(host, hass, add_devices_callback, filename):
|
||||
""" Setup a phue bridge based on host parameter. """
|
||||
import phue
|
||||
|
||||
try:
|
||||
bridge = phue.Bridge(
|
||||
host,
|
||||
config_file_path=hass.config.path(PHUE_CONFIG_FILE))
|
||||
config_file_path=hass.config.path(filename))
|
||||
except ConnectionRefusedError: # Wrong host was given
|
||||
_LOGGER.exception("Error connecting to the Hue bridge at %s", host)
|
||||
|
||||
@@ -88,7 +90,7 @@ def setup_bridge(host, hass, add_devices_callback):
|
||||
except phue.PhueRegistrationException:
|
||||
_LOGGER.warning("Connected to Hue at %s but not registered.", host)
|
||||
|
||||
request_configuration(host, hass, add_devices_callback)
|
||||
request_configuration(host, hass, add_devices_callback, filename)
|
||||
|
||||
return
|
||||
|
||||
@@ -120,10 +122,17 @@ def setup_bridge(host, hass, add_devices_callback):
|
||||
|
||||
new_lights = []
|
||||
|
||||
api_name = api.get('config').get('name')
|
||||
if api_name == 'RaspBee-GW':
|
||||
bridge_type = 'deconz'
|
||||
else:
|
||||
bridge_type = 'hue'
|
||||
|
||||
for light_id, info in api_states.items():
|
||||
if light_id not in lights:
|
||||
lights[light_id] = HueLight(int(light_id), info,
|
||||
bridge, update_lights)
|
||||
bridge, update_lights,
|
||||
bridge_type=bridge_type)
|
||||
new_lights.append(lights[light_id])
|
||||
else:
|
||||
lights[light_id].info = info
|
||||
@@ -134,7 +143,7 @@ def setup_bridge(host, hass, add_devices_callback):
|
||||
update_lights()
|
||||
|
||||
|
||||
def request_configuration(host, hass, add_devices_callback):
|
||||
def request_configuration(host, hass, add_devices_callback, filename):
|
||||
""" Request configuration steps from the user. """
|
||||
configurator = get_component('configurator')
|
||||
|
||||
@@ -148,7 +157,7 @@ def request_configuration(host, hass, add_devices_callback):
|
||||
# pylint: disable=unused-argument
|
||||
def hue_configuration_callback(data):
|
||||
""" Actions to do when our configuration callback is called. """
|
||||
setup_bridge(host, hass, add_devices_callback)
|
||||
setup_bridge(host, hass, add_devices_callback, filename)
|
||||
|
||||
_CONFIGURING[host] = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
@@ -162,11 +171,14 @@ def request_configuration(host, hass, add_devices_callback):
|
||||
class HueLight(Light):
|
||||
""" Represents a Hue light """
|
||||
|
||||
def __init__(self, light_id, info, bridge, update_lights):
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, light_id, info, bridge, update_lights,
|
||||
bridge_type='hue'):
|
||||
self.light_id = light_id
|
||||
self.info = info
|
||||
self.bridge = bridge
|
||||
self.update_lights = update_lights
|
||||
self.bridge_type = bridge_type
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -226,14 +238,17 @@ class HueLight(Light):
|
||||
command['alert'] = 'lselect'
|
||||
elif flash == FLASH_SHORT:
|
||||
command['alert'] = 'select'
|
||||
else:
|
||||
elif self.bridge_type == 'hue':
|
||||
command['alert'] = 'none'
|
||||
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
|
||||
if effect == EFFECT_COLORLOOP:
|
||||
command['effect'] = 'colorloop'
|
||||
else:
|
||||
elif effect == EFFECT_RANDOM:
|
||||
command['hue'] = random.randrange(0, 65535)
|
||||
command['sat'] = random.randrange(150, 254)
|
||||
elif self.bridge_type == 'hue':
|
||||
command['effect'] = 'none'
|
||||
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
||||
18
homeassistant/components/light/insteon_hub.py
Normal file
18
homeassistant/components/light/insteon_hub.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
homeassistant.components.light.insteon
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Insteon Hub lights.
|
||||
"""
|
||||
|
||||
from homeassistant.components.insteon_hub import (INSTEON, InsteonToggleDevice)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Insteon Hub light platform. """
|
||||
devs = []
|
||||
for device in INSTEON.devices:
|
||||
if device.DeviceCategory == "Switched Lighting Control":
|
||||
devs.append(InsteonToggleDevice(device))
|
||||
if device.DeviceCategory == "Dimmable Lighting Control":
|
||||
devs.append(InsteonToggleDevice(device))
|
||||
add_devices(devs)
|
||||
267
homeassistant/components/light/lifx.py
Normal file
267
homeassistant/components/light/lifx.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
homeassistant.components.light.lifx
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
LIFX platform that implements lights
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.lifx/
|
||||
"""
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
import logging
|
||||
import colorsys
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
from homeassistant.components.light import \
|
||||
(Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['liffylights==0.9.4']
|
||||
DEPENDENCIES = []
|
||||
|
||||
CONF_SERVER = "server" # server address configuration item
|
||||
CONF_BROADCAST = "broadcast" # broadcast address configuration item
|
||||
SHORT_MAX = 65535 # short int maximum
|
||||
BYTE_MAX = 255 # byte maximum
|
||||
TEMP_MIN = 2500 # lifx minimum temperature
|
||||
TEMP_MAX = 9000 # lifx maximum temperature
|
||||
TEMP_MIN_HASS = 154 # home assistant minimum temperature
|
||||
TEMP_MAX_HASS = 500 # home assistant maximum temperature
|
||||
|
||||
|
||||
class LIFX():
|
||||
def __init__(self, add_devices_callback,
|
||||
server_addr=None, broadcast_addr=None):
|
||||
import liffylights
|
||||
|
||||
self._devices = []
|
||||
|
||||
self._add_devices_callback = add_devices_callback
|
||||
|
||||
self._liffylights = liffylights.LiffyLights(
|
||||
self.on_device,
|
||||
self.on_power,
|
||||
self.on_color,
|
||||
server_addr,
|
||||
broadcast_addr)
|
||||
|
||||
def find_bulb(self, ipaddr):
|
||||
bulb = None
|
||||
for device in self._devices:
|
||||
if device.ipaddr == ipaddr:
|
||||
bulb = device
|
||||
break
|
||||
return bulb
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_device(self, ipaddr, name, power, hue, sat, bri, kel):
|
||||
bulb = self.find_bulb(ipaddr)
|
||||
|
||||
if bulb is None:
|
||||
_LOGGER.debug("new bulb %s %s %d %d %d %d %d",
|
||||
ipaddr, name, power, hue, sat, bri, kel)
|
||||
bulb = LIFXLight(self._liffylights, ipaddr, name,
|
||||
power, hue, sat, bri, kel)
|
||||
self._devices.append(bulb)
|
||||
self._add_devices_callback([bulb])
|
||||
else:
|
||||
_LOGGER.debug("update bulb %s %s %d %d %d %d %d",
|
||||
ipaddr, name, power, hue, sat, bri, kel)
|
||||
bulb.set_power(power)
|
||||
bulb.set_color(hue, sat, bri, kel)
|
||||
bulb.update_ha_state()
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_color(self, ipaddr, hue, sat, bri, kel):
|
||||
bulb = self.find_bulb(ipaddr)
|
||||
|
||||
if bulb is not None:
|
||||
bulb.set_color(hue, sat, bri, kel)
|
||||
bulb.update_ha_state()
|
||||
|
||||
def on_power(self, ipaddr, power):
|
||||
bulb = self.find_bulb(ipaddr)
|
||||
|
||||
if bulb is not None:
|
||||
bulb.set_power(power)
|
||||
bulb.update_ha_state()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def poll(self, now):
|
||||
self.probe()
|
||||
|
||||
def probe(self, address=None):
|
||||
self._liffylights.probe(address)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Set up platform. """
|
||||
server_addr = config.get(CONF_SERVER, None)
|
||||
broadcast_addr = config.get(CONF_BROADCAST, None)
|
||||
|
||||
lifx_library = LIFX(add_devices_callback, server_addr, broadcast_addr)
|
||||
|
||||
# register our poll service
|
||||
track_time_change(hass, lifx_library.poll, second=[10, 40])
|
||||
|
||||
lifx_library.probe()
|
||||
|
||||
|
||||
def convert_rgb_to_hsv(rgb):
|
||||
""" Convert HASS RGB values to HSV values. """
|
||||
red, green, blue = [_ / BYTE_MAX for _ in rgb]
|
||||
|
||||
hue, saturation, brightness = colorsys.rgb_to_hsv(red, green, blue)
|
||||
|
||||
return [int(hue * SHORT_MAX),
|
||||
int(saturation * SHORT_MAX),
|
||||
int(brightness * SHORT_MAX)]
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class LIFXLight(Light):
|
||||
""" Provides LIFX light. """
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, liffy, ipaddr, name, power, hue,
|
||||
saturation, brightness, kelvin):
|
||||
_LOGGER.debug("LIFXLight: %s %s",
|
||||
ipaddr, name)
|
||||
|
||||
self._liffylights = liffy
|
||||
self._ip = ipaddr
|
||||
self.set_name(name)
|
||||
self.set_power(power)
|
||||
self.set_color(hue, saturation, brightness, kelvin)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for LIFX light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def ipaddr(self):
|
||||
""" Returns the ip of the device. """
|
||||
return self._ip
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
""" Returns RGB value. """
|
||||
_LOGGER.debug("rgb_color: [%d %d %d]",
|
||||
self._rgb[0], self._rgb[1], self._rgb[2])
|
||||
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
""" Returns brightness of this light between 0..255. """
|
||||
brightness = int(self._bri / (BYTE_MAX + 1))
|
||||
|
||||
_LOGGER.debug("brightness: %d",
|
||||
brightness)
|
||||
|
||||
return brightness
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
""" Returns color temperature. """
|
||||
temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) *
|
||||
(self._kel - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
|
||||
|
||||
_LOGGER.debug("color_temp: %d",
|
||||
temperature)
|
||||
|
||||
return temperature
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
_LOGGER.debug("is_on: %d",
|
||||
self._power)
|
||||
|
||||
return self._power != 0
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = kwargs[ATTR_TRANSITION] * 1000
|
||||
else:
|
||||
fade = 0
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
hue, saturation, brightness = \
|
||||
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
|
||||
else:
|
||||
hue = self._hue
|
||||
saturation = self._sat
|
||||
brightness = self._bri
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
|
||||
else:
|
||||
brightness = self._bri
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
kelvin = int(((TEMP_MAX - TEMP_MIN) *
|
||||
(kwargs[ATTR_COLOR_TEMP] - TEMP_MIN_HASS) /
|
||||
(TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN)
|
||||
else:
|
||||
kelvin = self._kel
|
||||
|
||||
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
|
||||
self._ip, self._power,
|
||||
hue, saturation, brightness, kelvin, fade)
|
||||
|
||||
if self._power == 0:
|
||||
self._liffylights.set_power(self._ip, 65535, fade)
|
||||
|
||||
self._liffylights.set_color(self._ip, hue, saturation,
|
||||
brightness, kelvin, fade)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = kwargs[ATTR_TRANSITION] * 1000
|
||||
else:
|
||||
fade = 0
|
||||
|
||||
_LOGGER.debug("turn_off: %s %d",
|
||||
self._ip, fade)
|
||||
|
||||
self._liffylights.set_power(self._ip, 0, fade)
|
||||
|
||||
def set_name(self, name):
|
||||
""" Set name. """
|
||||
self._name = name
|
||||
|
||||
def set_power(self, power):
|
||||
""" Set power state value. """
|
||||
_LOGGER.debug("set_power: %d",
|
||||
power)
|
||||
|
||||
self._power = (power != 0)
|
||||
|
||||
def set_color(self, hue, sat, bri, kel):
|
||||
""" Set color state values. """
|
||||
self._hue = hue
|
||||
self._sat = sat
|
||||
self._bri = bri
|
||||
self._kel = kel
|
||||
|
||||
red, green, blue = colorsys.hsv_to_rgb(hue / SHORT_MAX,
|
||||
sat / SHORT_MAX,
|
||||
bri / SHORT_MAX)
|
||||
|
||||
red = int(red * BYTE_MAX)
|
||||
green = int(green * BYTE_MAX)
|
||||
blue = int(blue * BYTE_MAX)
|
||||
|
||||
_LOGGER.debug("set_color: %d %d %d %d [%d %d %d]",
|
||||
hue, sat, bri, kel, red, green, blue)
|
||||
|
||||
self._rgb = [red, green, blue]
|
||||
226
homeassistant/components/light/mysensors.py
Normal file
226
homeassistant/components/light/mysensors.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
homeassistant.components.light.mysensors.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for MySensors lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.mysensors.html
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR)
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
STATE_ON, STATE_OFF)
|
||||
|
||||
import homeassistant.components.mysensors as mysensors
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ATTR_RGB_WHITE = 'rgb_white'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for sensors."""
|
||||
# Only act if loaded via mysensors by discovery event.
|
||||
# Otherwise gateway is not setup.
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||
# states. Map them in a dict of lists.
|
||||
pres = gateway.const.Presentation
|
||||
set_req = gateway.const.SetReq
|
||||
map_sv_types = {
|
||||
pres.S_LIGHT: [set_req.V_LIGHT],
|
||||
pres.S_DIMMER: [set_req.V_DIMMER],
|
||||
}
|
||||
if float(gateway.version) >= 1.5:
|
||||
# Add V_RGBW when rgb_white is implemented in the frontend
|
||||
map_sv_types.update({
|
||||
pres.S_RGB_LIGHT: [set_req.V_RGB],
|
||||
})
|
||||
map_sv_types[pres.S_LIGHT].append(set_req.V_STATUS)
|
||||
map_sv_types[pres.S_DIMMER].append(set_req.V_PERCENTAGE)
|
||||
|
||||
devices = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
map_sv_types, devices, add_devices, MySensorsLight))
|
||||
|
||||
|
||||
class MySensorsLight(Light):
|
||||
"""Represent the value of a MySensors child node."""
|
||||
|
||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||
|
||||
def __init__(self, gateway, node_id, child_id, name, value_type):
|
||||
"""Setup instance attributes."""
|
||||
self.gateway = gateway
|
||||
self.node_id = node_id
|
||||
self.child_id = child_id
|
||||
self._name = name
|
||||
self.value_type = value_type
|
||||
self.battery_level = 0
|
||||
self._values = {}
|
||||
self._state = None
|
||||
self._rgb = None
|
||||
self._brightness = None
|
||||
self._white = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""MySensor gateway pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of this entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""RGB color value [int, int, int]."""
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def rgb_white(self): # not implemented in the frontend yet
|
||||
"""White value in RGBW, value between 0..255."""
|
||||
return self._white
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
device_attr = {
|
||||
mysensors.ATTR_PORT: self.gateway.port,
|
||||
mysensors.ATTR_NODE_ID: self.node_id,
|
||||
mysensors.ATTR_CHILD_ID: self.child_id,
|
||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||
}
|
||||
for value_type, value in self._values.items():
|
||||
device_attr[self.gateway.const.SetReq(value_type).name] = value
|
||||
return device_attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if device is on."""
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
rgb = self._rgb
|
||||
brightness = self._brightness
|
||||
white = self._white
|
||||
|
||||
if set_req.V_LIGHT in self._values and not self._state:
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, set_req.V_LIGHT, 1)
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs and set_req.V_DIMMER in self._values and \
|
||||
kwargs[ATTR_BRIGHTNESS] != self._brightness:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
percent = round(100 * brightness / 255)
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, set_req.V_DIMMER, percent)
|
||||
|
||||
if float(self.gateway.version) >= 1.5:
|
||||
|
||||
if ATTR_RGB_WHITE in kwargs and \
|
||||
self.value_type in (set_req.V_RGB, set_req.V_RGBW) and \
|
||||
kwargs[ATTR_RGB_WHITE] != self._white:
|
||||
white = kwargs[ATTR_RGB_WHITE]
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs and \
|
||||
self.value_type in (set_req.V_RGB, set_req.V_RGBW) and \
|
||||
kwargs[ATTR_RGB_COLOR] != self._rgb:
|
||||
rgb = kwargs[ATTR_RGB_COLOR]
|
||||
if set_req.V_RGBW == self.value_type:
|
||||
hex_template = '%02x%02x%02x%02x'
|
||||
color_list = rgb.append(white)
|
||||
if set_req.V_RGB == self.value_type:
|
||||
hex_template = '%02x%02x%02x'
|
||||
color_list = rgb
|
||||
hex_color = hex_template % tuple(color_list)
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, self.value_type, hex_color)
|
||||
|
||||
if self.gateway.optimistic:
|
||||
# optimistically assume that light has changed state
|
||||
self._state = True
|
||||
self._rgb = rgb
|
||||
self._brightness = brightness
|
||||
self._white = white
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
v_type = set_req.V_LIGHT
|
||||
value = 0
|
||||
if set_req.V_LIGHT in self._values:
|
||||
self._values[set_req.V_LIGHT] = STATE_OFF
|
||||
elif set_req.V_DIMMER in self._values:
|
||||
v_type = set_req.V_DIMMER
|
||||
elif float(self.gateway.version) >= 1.5:
|
||||
if set_req.V_RGB in self._values:
|
||||
v_type = set_req.V_RGB
|
||||
value = '000000'
|
||||
elif set_req.V_RGBW in self._values:
|
||||
v_type = set_req.V_RGBW
|
||||
value = '00000000'
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, v_type, value)
|
||||
|
||||
if self.gateway.optimistic:
|
||||
# optimistically assume that light has changed state
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.value_type in self._values
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
child = node.children[self.child_id]
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.battery_level = node.battery_level
|
||||
for value_type, value in child.values.items():
|
||||
_LOGGER.debug(
|
||||
"%s: value_type %s, value = %s", self._name, value_type, value)
|
||||
if value_type == set_req.V_LIGHT:
|
||||
self._values[value_type] = (
|
||||
STATE_ON if int(value) == 1 else STATE_OFF)
|
||||
self._state = self._values[value_type] == STATE_ON
|
||||
else:
|
||||
self._values[value_type] = value
|
||||
if value_type == set_req.V_DIMMER:
|
||||
self._brightness = round(
|
||||
255 * int(self._values[value_type]) / 100)
|
||||
if self._brightness == 0:
|
||||
self._state = False
|
||||
if set_req.V_LIGHT not in self._values:
|
||||
self._state = self._brightness > 0
|
||||
if float(self.gateway.version) >= 1.5 and \
|
||||
value_type in (set_req.V_RGB, set_req.V_RGBW):
|
||||
# convert hex color string to rgb(w) integer list
|
||||
color_list = [int(value[i:i + len(value) // 3], 16)
|
||||
for i in range(0,
|
||||
len(value),
|
||||
len(value) // 3)]
|
||||
if len(color_list) > 3:
|
||||
self._white = color_list.pop()
|
||||
self._rgb = color_list
|
||||
if set_req.V_LIGHT not in self._values or \
|
||||
set_req.V_DIMMER not in self._values:
|
||||
self._state = max(color_list) > 0
|
||||
@@ -9,12 +9,13 @@ https://home-assistant.io/components/light.rfxtrx/
|
||||
import logging
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \
|
||||
ATTR_NAME, EVENT_BUTTON_PRESSED
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID,
|
||||
ATTR_NAME, EVENT_BUTTON_PRESSED)
|
||||
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
@@ -49,7 +50,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
def light_update(event):
|
||||
""" Callback for light updates from the RFXtrx gateway. """
|
||||
if not isinstance(event.device, rfxtrxmod.LightingDevice):
|
||||
if not isinstance(event.device, rfxtrxmod.LightingDevice) or \
|
||||
not event.device.known_to_be_dimmable:
|
||||
return
|
||||
|
||||
# Add entity if not exist and the automatic_add is True
|
||||
@@ -73,13 +75,13 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
add_devices_callback([new_light])
|
||||
|
||||
# Check if entity exists or previously added automatically
|
||||
if entity_id in rfxtrx.RFX_DEVICES \
|
||||
and isinstance(rfxtrx.RFX_DEVICES[entity_id], RfxtrxLight):
|
||||
if entity_id in rfxtrx.RFX_DEVICES:
|
||||
_LOGGER.debug(
|
||||
"EntityID: %s light_update. Command: %s",
|
||||
entity_id,
|
||||
event.values['Command']
|
||||
)
|
||||
|
||||
if event.values['Command'] == 'On'\
|
||||
or event.values['Command'] == 'Off':
|
||||
|
||||
@@ -89,15 +91,27 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
rfxtrx.RFX_DEVICES[entity_id]._state = is_on
|
||||
rfxtrx.RFX_DEVICES[entity_id].update_ha_state()
|
||||
|
||||
# Fire event
|
||||
if rfxtrx.RFX_DEVICES[entity_id].should_fire_event:
|
||||
rfxtrx.RFX_DEVICES[entity_id].hass.bus.fire(
|
||||
EVENT_BUTTON_PRESSED, {
|
||||
ATTR_ENTITY_ID:
|
||||
rfxtrx.RFX_DEVICES[entity_id].entity_id,
|
||||
ATTR_STATE: event.values['Command'].lower()
|
||||
}
|
||||
)
|
||||
elif event.values['Command'] == 'Set level':
|
||||
# pylint: disable=protected-access
|
||||
rfxtrx.RFX_DEVICES[entity_id]._brightness = \
|
||||
(event.values['Dim level'] * 255 // 100)
|
||||
|
||||
# Update the rfxtrx device state
|
||||
is_on = rfxtrx.RFX_DEVICES[entity_id]._brightness > 0
|
||||
rfxtrx.RFX_DEVICES[entity_id]._state = is_on
|
||||
rfxtrx.RFX_DEVICES[entity_id].update_ha_state()
|
||||
else:
|
||||
return
|
||||
|
||||
# Fire event
|
||||
if rfxtrx.RFX_DEVICES[entity_id].should_fire_event:
|
||||
rfxtrx.RFX_DEVICES[entity_id].hass.bus.fire(
|
||||
EVENT_BUTTON_PRESSED, {
|
||||
ATTR_ENTITY_ID:
|
||||
rfxtrx.RFX_DEVICES[entity_id].entity_id,
|
||||
ATTR_STATE: event.values['Command'].lower()
|
||||
}
|
||||
)
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
@@ -111,6 +125,7 @@ class RfxtrxLight(Light):
|
||||
self._event = event
|
||||
self._state = datas[ATTR_STATE]
|
||||
self._should_fire_event = datas[ATTR_FIREEVENT]
|
||||
self._brightness = 0
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -132,12 +147,25 @@ class RfxtrxLight(Light):
|
||||
""" True if light is on. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
""" Brightness of this light between 0..255. """
|
||||
return self._brightness
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the light on. """
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
|
||||
if brightness is None:
|
||||
self._brightness = 100
|
||||
else:
|
||||
self._brightness = ((brightness + 4) * 100 // 255 - 1)
|
||||
|
||||
if hasattr(self, '_event') and self._event:
|
||||
self._event.device.send_on(rfxtrx.RFXOBJECT.transport)
|
||||
self._event.device.send_dim(rfxtrx.RFXOBJECT.transport,
|
||||
self._brightness)
|
||||
|
||||
self._brightness = (self._brightness * 255 // 100)
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
@@ -147,5 +175,6 @@ class RfxtrxLight(Light):
|
||||
if hasattr(self, '_event') and self._event:
|
||||
self._event.device.send_off(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._brightness = 0
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
|
||||
118
homeassistant/components/light/scsgate.py
Normal file
118
homeassistant/components/light/scsgate.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
homeassistant.components.light.scsgate
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for SCSGate lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.scsgate/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.scsgate as scsgate
|
||||
|
||||
from homeassistant.components.light import Light
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
|
||||
DEPENDENCIES = ['scsgate']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Add the SCSGate swiches defined inside of the configuration file. """
|
||||
|
||||
devices = config.get('devices')
|
||||
lights = []
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if devices:
|
||||
for _, entity_info in devices.items():
|
||||
if entity_info['scs_id'] in scsgate.SCSGATE.devices:
|
||||
continue
|
||||
|
||||
logger.info("Adding %s scsgate.light", entity_info['name'])
|
||||
|
||||
name = entity_info['name']
|
||||
scs_id = entity_info['scs_id']
|
||||
light = SCSGateLight(
|
||||
name=name,
|
||||
scs_id=scs_id,
|
||||
logger=logger)
|
||||
lights.append(light)
|
||||
|
||||
add_devices_callback(lights)
|
||||
scsgate.SCSGATE.add_devices_to_register(lights)
|
||||
|
||||
|
||||
class SCSGateLight(Light):
|
||||
""" Provides a SCSGate light. """
|
||||
def __init__(self, scs_id, name, logger):
|
||||
self._name = name
|
||||
self._scs_id = scs_id
|
||||
self._toggled = False
|
||||
self._logger = logger
|
||||
|
||||
@property
|
||||
def scs_id(self):
|
||||
""" SCS ID """
|
||||
return self._scs_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a SCSGate light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if light is on. """
|
||||
return self._toggled
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
from scsgate.tasks import ToggleStatusTask
|
||||
|
||||
scsgate.SCSGATE.append_task(
|
||||
ToggleStatusTask(
|
||||
target=self._scs_id,
|
||||
toggled=True))
|
||||
|
||||
self._toggled = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
from scsgate.tasks import ToggleStatusTask
|
||||
|
||||
scsgate.SCSGATE.append_task(
|
||||
ToggleStatusTask(
|
||||
target=self._scs_id,
|
||||
toggled=False))
|
||||
|
||||
self._toggled = False
|
||||
self.update_ha_state()
|
||||
|
||||
def process_event(self, message):
|
||||
""" Handle a SCSGate message related with this light """
|
||||
if self._toggled == message.toggled:
|
||||
self._logger.info(
|
||||
"Light %s, ignoring message %s because state already active",
|
||||
self._scs_id, message)
|
||||
# Nothing changed, ignoring
|
||||
return
|
||||
|
||||
self._toggled = message.toggled
|
||||
self.update_ha_state()
|
||||
|
||||
command = "off"
|
||||
if self._toggled:
|
||||
command = "on"
|
||||
|
||||
self.hass.bus.fire(
|
||||
'button_pressed', {
|
||||
ATTR_ENTITY_ID: self._scs_id,
|
||||
'state': command
|
||||
}
|
||||
)
|
||||
@@ -42,6 +42,7 @@ turn_on:
|
||||
description: Light effect
|
||||
values:
|
||||
- colorloop
|
||||
- random
|
||||
|
||||
turn_off:
|
||||
description: Turn a light off
|
||||
@@ -54,3 +55,15 @@ turn_off:
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to next state
|
||||
example: 60
|
||||
|
||||
toggle:
|
||||
description: Toggles a light
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to toggle
|
||||
example: 'light.kitchen'
|
||||
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to next state
|
||||
example: 60
|
||||
|
||||
@@ -7,16 +7,22 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.vera/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from homeassistant.components.switch.vera import VeraSwitch
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
||||
|
||||
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/'
|
||||
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
|
||||
'#python-vera==0.1.1']
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
ATTR_TRIPPED,
|
||||
ATTR_ARMED,
|
||||
ATTR_LAST_TRIP_TIME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_ON,
|
||||
STATE_OFF)
|
||||
|
||||
REQUIREMENTS = ['pyvera==0.2.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,10 +42,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
device_data = config.get('device_data', {})
|
||||
|
||||
controller = veraApi.VeraController(base_url)
|
||||
vera_controller, created = veraApi.init_controller(base_url)
|
||||
|
||||
if created:
|
||||
def stop_subscription(event):
|
||||
""" Shutdown Vera subscriptions and subscription thread on exit"""
|
||||
_LOGGER.info("Shutting down subscriptions.")
|
||||
vera_controller.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
|
||||
|
||||
devices = []
|
||||
try:
|
||||
devices = controller.get_devices([
|
||||
devices = vera_controller.get_devices([
|
||||
'Switch',
|
||||
'On/Off Switch',
|
||||
'Dimmable Switch'])
|
||||
@@ -50,26 +65,44 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
lights = []
|
||||
for device in devices:
|
||||
extra_data = device_data.get(device.deviceId, {})
|
||||
extra_data = device_data.get(device.device_id, {})
|
||||
exclude = extra_data.get('exclude', False)
|
||||
|
||||
if exclude is not True:
|
||||
lights.append(VeraLight(device, extra_data))
|
||||
lights.append(VeraLight(device, vera_controller, extra_data))
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
|
||||
class VeraLight(VeraSwitch):
|
||||
class VeraLight(Light):
|
||||
""" Represents a Vera Light, including dimmable. """
|
||||
|
||||
def __init__(self, vera_device, controller, extra_data=None):
|
||||
self.vera_device = vera_device
|
||||
self.extra_data = extra_data
|
||||
self.controller = controller
|
||||
if self.extra_data and self.extra_data.get('name'):
|
||||
self._name = self.extra_data.get('name')
|
||||
else:
|
||||
self._name = self.vera_device.name
|
||||
self._state = STATE_OFF
|
||||
|
||||
self.controller.register(vera_device, self._update_callback)
|
||||
self.update()
|
||||
|
||||
def _update_callback(self, _device):
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes or {}
|
||||
def name(self):
|
||||
""" Get the mame of the switch. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of the light."""
|
||||
if self.vera_device.is_dimmable:
|
||||
attr[ATTR_BRIGHTNESS] = self.vera_device.get_brightness()
|
||||
|
||||
return attr
|
||||
return self.vera_device.get_brightness()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable:
|
||||
@@ -77,5 +110,51 @@ class VeraLight(VeraSwitch):
|
||||
else:
|
||||
self.vera_device.switch_on()
|
||||
|
||||
self.last_command_send = time.time()
|
||||
self.is_on_status = True
|
||||
self._state = STATE_ON
|
||||
self.update_ha_state(True)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
self.vera_device.switch_off()
|
||||
self._state = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
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] = dt_util.datetime_to_str(
|
||||
utc_time)
|
||||
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
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant not to poll this entity. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state == STATE_ON
|
||||
|
||||
def update(self):
|
||||
""" Called by the vera device callback to update state. """
|
||||
if self.vera_device.is_switched_on():
|
||||
self._state = STATE_ON
|
||||
else:
|
||||
self._state = STATE_OFF
|
||||
|
||||
@@ -8,11 +8,10 @@ https://home-assistant.io/components/light.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.3.1']
|
||||
REQUIREMENTS = ['python-wink==0.6.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
@@ -34,9 +33,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
WinkLight(light) for light in pywink.get_bulbs())
|
||||
|
||||
|
||||
class WinkLight(WinkToggleDevice):
|
||||
class WinkLight(Light):
|
||||
""" Represents a Wink light. """
|
||||
|
||||
def __init__(self, wink):
|
||||
self.wink = wink
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this Wink switch. """
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the light if any. """
|
||||
return self.wink.name()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if light is on. """
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of the light."""
|
||||
return int(self.wink.brightness() * 255)
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
@@ -48,14 +70,10 @@ class WinkLight(WinkToggleDevice):
|
||||
else:
|
||||
self.wink.set_state(True)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
self.wink.set_state(False)
|
||||
|
||||
if self.is_on:
|
||||
brightness = self.wink.brightness()
|
||||
|
||||
if brightness is not None:
|
||||
attr[ATTR_BRIGHTNESS] = int(brightness * 255)
|
||||
|
||||
return attr
|
||||
def update(self):
|
||||
""" Update state of the light. """
|
||||
self.wink.update_state()
|
||||
|
||||
29
homeassistant/components/light/zigbee.py
Normal file
29
homeassistant/components/light/zigbee.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
homeassistant.components.light.zigbee
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Contains functionality to use a ZigBee device as a light.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.zigbee/
|
||||
"""
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.components.zigbee import (
|
||||
ZigBeeDigitalOut, ZigBeeDigitalOutConfig)
|
||||
|
||||
|
||||
DEPENDENCIES = ["zigbee"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
""" Create and add an entity based on the configuration. """
|
||||
add_entities([
|
||||
ZigBeeLight(hass, ZigBeeDigitalOutConfig(config))
|
||||
])
|
||||
|
||||
|
||||
class ZigBeeLight(ZigBeeDigitalOut, Light):
|
||||
"""
|
||||
Use multiple inheritance to turn an instance of ZigBeeDigitalOut into a
|
||||
Light.
|
||||
"""
|
||||
pass
|
||||
@@ -11,8 +11,10 @@ https://home-assistant.io/components/light.zwave/
|
||||
from threading import Timer
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.light import (Light, ATTR_BRIGHTNESS)
|
||||
import homeassistant.components.zwave as zwave
|
||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS, DOMAIN
|
||||
from homeassistant.components.zwave import (
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL, TYPE_BYTE, GENRE_USER, NETWORK,
|
||||
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -20,14 +22,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
node = NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||
if value.command_class != COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||
return
|
||||
if value.type != zwave.TYPE_BYTE:
|
||||
if value.type != TYPE_BYTE:
|
||||
return
|
||||
if value.genre != zwave.GENRE_USER:
|
||||
if value.genre != GENRE_USER:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
@@ -45,15 +47,14 @@ def brightness_state(value):
|
||||
return 255, STATE_OFF
|
||||
|
||||
|
||||
class ZwaveDimmer(Light):
|
||||
class ZwaveDimmer(ZWaveDeviceEntity, Light):
|
||||
""" Provides a Z-Wave dimmer. """
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, value):
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
||||
self._value = value
|
||||
self._node = value.node
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
|
||||
self._brightness, self._state = brightness_state(value)
|
||||
|
||||
@@ -86,18 +87,6 @@ class ZwaveDimmer(Light):
|
||||
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
name = self._node.name or "{}".format(self._node.product_name)
|
||||
|
||||
return "{}".format(name or self._value.label)
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
""" Brightness of this light between 0..255. """
|
||||
@@ -118,10 +107,10 @@ class ZwaveDimmer(Light):
|
||||
# brightness.
|
||||
brightness = (self._brightness / 255) * 99
|
||||
|
||||
if self._node.set_dimmer(self._value.value_id, brightness):
|
||||
if self._value.node.set_dimmer(self._value.value_id, brightness):
|
||||
self._state = STATE_ON
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
if self._node.set_dimmer(self._value.value_id, 0):
|
||||
if self._value.node.set_dimmer(self._value.value_id, 0):
|
||||
self._state = STATE_OFF
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.components import (group, wink)
|
||||
from homeassistant.components import (group, verisure, wink)
|
||||
|
||||
DOMAIN = 'lock'
|
||||
SCAN_INTERVAL = 30
|
||||
@@ -28,12 +28,15 @@ ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks')
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
ATTR_LOCKED = "locked"
|
||||
ATTR_CODE = 'code'
|
||||
ATTR_CODE_FORMAT = 'code_format'
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_LOCKS: 'wink'
|
||||
wink.DISCOVER_LOCKS: 'wink',
|
||||
verisure.DISCOVER_LOCKS: 'verisure'
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -45,15 +48,25 @@ def is_locked(hass, entity_id=None):
|
||||
return hass.states.is_state(entity_id, STATE_LOCKED)
|
||||
|
||||
|
||||
def lock(hass, entity_id=None):
|
||||
def lock(hass, entity_id=None, code=None):
|
||||
""" Locks all or specified locks. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_LOCK, data)
|
||||
|
||||
|
||||
def unlock(hass, entity_id=None):
|
||||
def unlock(hass, entity_id=None, code=None):
|
||||
""" Unlocks all or specified locks. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_UNLOCK, data)
|
||||
|
||||
|
||||
@@ -68,11 +81,16 @@ def setup(hass, config):
|
||||
""" Handles calls to the lock services. """
|
||||
target_locks = component.extract_from_service(service)
|
||||
|
||||
if ATTR_CODE not in service.data:
|
||||
code = None
|
||||
else:
|
||||
code = service.data[ATTR_CODE]
|
||||
|
||||
for item in target_locks:
|
||||
if service.service == SERVICE_LOCK:
|
||||
item.lock()
|
||||
item.lock(code=code)
|
||||
else:
|
||||
item.unlock()
|
||||
item.unlock(code=code)
|
||||
|
||||
if item.should_poll:
|
||||
item.update_ha_state(True)
|
||||
@@ -91,19 +109,34 @@ class LockDevice(Entity):
|
||||
""" Represents a lock within Home Assistant. """
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" regex for code format or None if no code is required. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
""" Is the lock locked or unlocked. """
|
||||
return None
|
||||
|
||||
def lock(self):
|
||||
def lock(self, **kwargs):
|
||||
""" Locks the lock. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def unlock(self):
|
||||
def unlock(self, **kwargs):
|
||||
""" Unlocks the lock. """
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
if self.code_format is None:
|
||||
return None
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
}
|
||||
return state_attr
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
locked = self.is_locked
|
||||
|
||||
92
homeassistant/components/lock/verisure.py
Normal file
92
homeassistant/components/lock/verisure.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
homeassistant.components.lock.verisure
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Interfaces with Verisure locks.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/verisure/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.verisure as verisure
|
||||
from homeassistant.components.lock import LockDevice
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_LOCKED, STATE_UNLOCKED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ATTR_CODE = 'code'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Verisure platform. """
|
||||
|
||||
if not verisure.MY_PAGES:
|
||||
_LOGGER.error('A connection has not been made to Verisure mypages.')
|
||||
return False
|
||||
|
||||
locks = []
|
||||
|
||||
locks.extend([VerisureDoorlock(value)
|
||||
for value in verisure.LOCK_STATUS.values()
|
||||
if verisure.SHOW_LOCKS])
|
||||
|
||||
add_devices(locks)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VerisureDoorlock(LockDevice):
|
||||
""" Represents a Verisure doorlock status. """
|
||||
|
||||
def __init__(self, lock_status, code=None):
|
||||
self._id = lock_status.id
|
||||
self._state = STATE_UNKNOWN
|
||||
self._code = code
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return 'Lock {}'.format(self._id)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" Six digit code required. """
|
||||
return '^\\d{%s}$' % verisure.CODE_DIGITS
|
||||
|
||||
def update(self):
|
||||
""" Update lock status """
|
||||
verisure.update_lock()
|
||||
|
||||
if verisure.LOCK_STATUS[self._id].status == 'unlocked':
|
||||
self._state = STATE_UNLOCKED
|
||||
elif verisure.LOCK_STATUS[self._id].status == 'locked':
|
||||
self._state = STATE_LOCKED
|
||||
elif verisure.LOCK_STATUS[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
'Unknown lock state %s',
|
||||
verisure.LOCK_STATUS[self._id].status)
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
""" True if device is locked. """
|
||||
return verisure.LOCK_STATUS[self._id].status
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
""" Send unlock command. """
|
||||
verisure.MY_PAGES.lock.set(kwargs[ATTR_CODE], self._id, 'UNLOCKED')
|
||||
_LOGGER.info('verisure doorlock unlocking')
|
||||
verisure.MY_PAGES.lock.wait_while_pending()
|
||||
verisure.update_lock()
|
||||
|
||||
def lock(self, **kwargs):
|
||||
""" Send lock command. """
|
||||
verisure.MY_PAGES.lock.set(kwargs[ATTR_CODE], self._id, 'LOCKED')
|
||||
_LOGGER.info('verisure doorlock locking')
|
||||
verisure.MY_PAGES.lock.wait_while_pending()
|
||||
verisure.update_lock()
|
||||
@@ -11,7 +11,7 @@ import logging
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.3.1']
|
||||
REQUIREMENTS = ['python-wink==0.6.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
@@ -6,6 +6,7 @@ Parses events and generates a human log.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/logbook/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
import re
|
||||
@@ -14,10 +15,10 @@ from homeassistant.core import State, DOMAIN as HA_DOMAIN
|
||||
from homeassistant.const import (
|
||||
EVENT_STATE_CHANGED, STATE_NOT_HOME, STATE_ON, STATE_OFF,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
|
||||
from homeassistant import util
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import recorder, sun
|
||||
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
from homeassistant.util import template
|
||||
|
||||
DOMAIN = "logbook"
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
@@ -28,7 +29,9 @@ QUERY_EVENTS_BETWEEN = """
|
||||
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
|
||||
"""
|
||||
|
||||
EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
|
||||
|
||||
GROUP_BY_MINUTES = 15
|
||||
|
||||
@@ -54,8 +57,22 @@ def log_entry(hass, name, message, domain=None, entity_id=None):
|
||||
|
||||
def setup(hass, config):
|
||||
""" Listens for download events to download files. """
|
||||
hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook)
|
||||
# create service handler
|
||||
def log_message(service):
|
||||
""" Handle sending notification message service calls. """
|
||||
message = service.data.get(ATTR_MESSAGE)
|
||||
name = service.data.get(ATTR_NAME)
|
||||
domain = service.data.get(ATTR_DOMAIN, None)
|
||||
entity_id = service.data.get(ATTR_ENTITY_ID, None)
|
||||
|
||||
if not message or not name:
|
||||
return
|
||||
|
||||
message = template.render(hass, message)
|
||||
log_entry(hass, name, message, domain, entity_id)
|
||||
|
||||
hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook)
|
||||
hass.services.register(DOMAIN, 'log', log_message)
|
||||
return True
|
||||
|
||||
|
||||
@@ -204,12 +221,12 @@ def humanify(events):
|
||||
event.time_fired, "Home Assistant", action,
|
||||
domain=HA_DOMAIN)
|
||||
|
||||
elif event.event_type == EVENT_LOGBOOK_ENTRY:
|
||||
elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY:
|
||||
domain = event.data.get(ATTR_DOMAIN)
|
||||
entity_id = event.data.get(ATTR_ENTITY_ID)
|
||||
if domain is None and entity_id is not None:
|
||||
try:
|
||||
domain = util.split_entity_id(str(entity_id))[0]
|
||||
domain = split_entity_id(str(entity_id))[0]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -76,8 +76,12 @@ def setup(hass, config=None):
|
||||
|
||||
logfilter[LOGGER_LOGS] = logs
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger.setLevel(logging.NOTSET)
|
||||
|
||||
# Set log filter for all log handler
|
||||
for handler in logging.root.handlers:
|
||||
handler.setLevel(logging.NOTSET)
|
||||
handler.addFilter(HomeAssistantLogFilter(logfilter))
|
||||
|
||||
return True
|
||||
|
||||
@@ -14,10 +14,10 @@ from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.const import (
|
||||
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING,
|
||||
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
|
||||
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_MUTE, SERVICE_TOGGLE,
|
||||
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
||||
|
||||
@@ -32,7 +32,6 @@ DISCOVERY_PLATFORMS = {
|
||||
discovery.SERVICE_PLEX: 'plex',
|
||||
}
|
||||
|
||||
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
|
||||
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
|
||||
@@ -68,16 +67,16 @@ SUPPORT_VOLUME_SET = 4
|
||||
SUPPORT_VOLUME_MUTE = 8
|
||||
SUPPORT_PREVIOUS_TRACK = 16
|
||||
SUPPORT_NEXT_TRACK = 32
|
||||
SUPPORT_YOUTUBE = 64
|
||||
|
||||
SUPPORT_TURN_ON = 128
|
||||
SUPPORT_TURN_OFF = 256
|
||||
SUPPORT_PLAY_MEDIA = 512
|
||||
|
||||
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
|
||||
SUPPORT_VOLUME_STEP = 1024
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_TURN_ON: 'turn_on',
|
||||
SERVICE_TURN_OFF: 'turn_off',
|
||||
SERVICE_TOGGLE: 'toggle',
|
||||
SERVICE_VOLUME_UP: 'volume_up',
|
||||
SERVICE_VOLUME_DOWN: 'volume_down',
|
||||
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
|
||||
@@ -130,6 +129,12 @@ def turn_off(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
|
||||
def toggle(hass, entity_id=None):
|
||||
""" Will toggle specified media player or all. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
|
||||
|
||||
|
||||
def volume_up(hass, entity_id=None):
|
||||
""" Send the media player the command for volume up. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
@@ -192,6 +197,13 @@ def media_previous_track(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
|
||||
|
||||
|
||||
def media_seek(hass, position, entity_id=None):
|
||||
""" Send the media player the command to seek in current playing media. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
data[ATTR_MEDIA_SEEK_POSITION] = position
|
||||
hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data)
|
||||
|
||||
|
||||
def play_media(hass, media_type, media_id, entity_id=None):
|
||||
""" Send the media player the command for playing media. """
|
||||
data = {"media_type": media_type, "media_id": media_id}
|
||||
@@ -275,7 +287,7 @@ def setup(hass, config):
|
||||
position = service.data[ATTR_MEDIA_SEEK_POSITION]
|
||||
|
||||
for player in target_players:
|
||||
player.seek(position)
|
||||
player.media_seek(position)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
@@ -283,20 +295,6 @@ def setup(hass, config):
|
||||
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service,
|
||||
descriptions.get(SERVICE_MEDIA_SEEK))
|
||||
|
||||
def play_youtube_video_service(service, media_id=None):
|
||||
""" Plays specified media_id on the media player. """
|
||||
if media_id is None:
|
||||
service.data.get('video')
|
||||
|
||||
if media_id is None:
|
||||
return
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.play_youtube(media_id)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
def play_media_service(service):
|
||||
""" Plays specified media_id on the media player. """
|
||||
media_type = service.data.get('media_type')
|
||||
@@ -314,20 +312,6 @@ def setup(hass, config):
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, "start_fireplace",
|
||||
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"),
|
||||
descriptions.get('start_fireplace'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, "start_epic_sax",
|
||||
lambda service: play_youtube_video_service(service, "kxopViU98Xo"),
|
||||
descriptions.get('start_epic_sax'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service,
|
||||
descriptions.get(SERVICE_YOUTUBE_VIDEO))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_PLAY_MEDIA, play_media_service,
|
||||
descriptions.get(SERVICE_PLAY_MEDIA))
|
||||
@@ -441,11 +425,6 @@ class MediaPlayerDevice(Entity):
|
||||
""" Flags of media commands that are supported. """
|
||||
return 0
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Extra attributes a device wants to expose. """
|
||||
return None
|
||||
|
||||
def turn_on(self):
|
||||
""" turn the media player on. """
|
||||
raise NotImplementedError()
|
||||
@@ -482,10 +461,6 @@ class MediaPlayerDevice(Entity):
|
||||
""" Send seek command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_youtube(self, media_id):
|
||||
""" Plays a YouTube media. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
""" Plays a piece of media. """
|
||||
raise NotImplementedError()
|
||||
@@ -521,16 +496,18 @@ class MediaPlayerDevice(Entity):
|
||||
""" Boolean if next track command supported. """
|
||||
return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK)
|
||||
|
||||
@property
|
||||
def support_youtube(self):
|
||||
""" Boolean if YouTube is supported. """
|
||||
return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
|
||||
|
||||
@property
|
||||
def support_play_media(self):
|
||||
""" Boolean if play media command supported. """
|
||||
return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA)
|
||||
|
||||
def toggle(self):
|
||||
""" Toggles the power on the media player. """
|
||||
if self.state in [STATE_OFF, STATE_IDLE]:
|
||||
self.turn_on()
|
||||
else:
|
||||
self.turn_off()
|
||||
|
||||
def volume_up(self):
|
||||
""" volume_up media player. """
|
||||
if self.volume_level < 1:
|
||||
@@ -564,9 +541,4 @@ class MediaPlayerDevice(Entity):
|
||||
if self.media_image_url:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr:
|
||||
state_attr.update(device_attr)
|
||||
|
||||
return state_attr
|
||||
|
||||
@@ -16,60 +16,57 @@ from homeassistant.const import (
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice,
|
||||
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE, SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
||||
|
||||
REQUIREMENTS = ['pychromecast==0.6.13']
|
||||
REQUIREMENTS = ['pychromecast==0.7.1']
|
||||
CONF_IGNORE_CEC = 'ignore_cec'
|
||||
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
|
||||
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE | SUPPORT_PLAY_MEDIA
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA
|
||||
KNOWN_HOSTS = []
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
cast = None
|
||||
DEFAULT_PORT = 8009
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the cast platform. """
|
||||
global cast
|
||||
import pychromecast
|
||||
cast = pychromecast
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# import CEC IGNORE attributes
|
||||
ignore_cec = config.get(CONF_IGNORE_CEC, [])
|
||||
if isinstance(ignore_cec, list):
|
||||
cast.IGNORE_CEC += ignore_cec
|
||||
pychromecast.IGNORE_CEC += ignore_cec
|
||||
else:
|
||||
logger.error('Chromecast conig, %s must be a list.', CONF_IGNORE_CEC)
|
||||
logger.error('CEC config "%s" must be a list.', CONF_IGNORE_CEC)
|
||||
|
||||
hosts = []
|
||||
|
||||
if discovery_info and discovery_info[0] not in KNOWN_HOSTS:
|
||||
hosts = [discovery_info[0]]
|
||||
if discovery_info and discovery_info in KNOWN_HOSTS:
|
||||
return
|
||||
|
||||
elif discovery_info:
|
||||
hosts = [discovery_info]
|
||||
|
||||
elif CONF_HOST in config:
|
||||
hosts = [config[CONF_HOST]]
|
||||
hosts = [(config[CONF_HOST], DEFAULT_PORT)]
|
||||
|
||||
else:
|
||||
hosts = (host_port[0] for host_port
|
||||
in cast.discover_chromecasts()
|
||||
if host_port[0] not in KNOWN_HOSTS)
|
||||
hosts = [tuple(dev[:2]) for dev in pychromecast.discover_chromecasts()
|
||||
if tuple(dev[:2]) not in KNOWN_HOSTS]
|
||||
|
||||
casts = []
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
casts.append(CastDevice(host))
|
||||
except cast.ChromecastConnectionError:
|
||||
pass
|
||||
else:
|
||||
casts.append(CastDevice(*host))
|
||||
KNOWN_HOSTS.append(host)
|
||||
except pychromecast.ChromecastConnectionError:
|
||||
pass
|
||||
|
||||
add_devices(casts)
|
||||
|
||||
@@ -80,11 +77,9 @@ class CastDevice(MediaPlayerDevice):
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, host):
|
||||
import pychromecast.controllers.youtube as youtube
|
||||
self.cast = cast.Chromecast(host)
|
||||
self.youtube = youtube.YouTubeController()
|
||||
self.cast.register_handler(self.youtube)
|
||||
def __init__(self, host, port):
|
||||
import pychromecast
|
||||
self.cast = pychromecast.Chromecast(host, port)
|
||||
|
||||
self.cast.socket_client.receiver_controller.register_status_listener(
|
||||
self)
|
||||
@@ -224,11 +219,13 @@ class CastDevice(MediaPlayerDevice):
|
||||
""" Turns on the ChromeCast. """
|
||||
# The only way we can turn the Chromecast is on is by launching an app
|
||||
if not self.cast.status or not self.cast.status.is_active_input:
|
||||
import pychromecast
|
||||
|
||||
if self.cast.app_id:
|
||||
self.cast.quit_app()
|
||||
|
||||
self.cast.play_media(
|
||||
CAST_SPLASH, cast.STREAM_TYPE_BUFFERED)
|
||||
CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns Chromecast off. """
|
||||
@@ -266,10 +263,6 @@ class CastDevice(MediaPlayerDevice):
|
||||
""" Plays media from a URL """
|
||||
self.cast.media_controller.play_media(media_id, media_type)
|
||||
|
||||
def play_youtube(self, media_id):
|
||||
""" Plays a YouTube media. """
|
||||
self.youtube.play_video(media_id)
|
||||
|
||||
# implementation of chromecast status_listener methods
|
||||
|
||||
def new_cast_status(self, status):
|
||||
|
||||
@@ -7,11 +7,11 @@ from homeassistant.const import (
|
||||
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT,
|
||||
MediaPlayerDevice,
|
||||
MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
||||
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE,
|
||||
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_NEXT_TRACK)
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@@ -26,9 +26,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
])
|
||||
|
||||
|
||||
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
|
||||
|
||||
YOUTUBE_PLAYER_SUPPORT = \
|
||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA
|
||||
|
||||
MUSIC_PLAYER_SUPPORT = \
|
||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
@@ -150,10 +152,9 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
|
||||
""" Flags of media commands that are supported. """
|
||||
return YOUTUBE_PLAYER_SUPPORT
|
||||
|
||||
def play_youtube(self, media_id):
|
||||
""" Plays a YouTube media. """
|
||||
def play_media(self, media_type, media_id):
|
||||
""" Plays a piece of media. """
|
||||
self.youtube_id = media_id
|
||||
self._media_title = 'some YouTube video'
|
||||
self.update_ha_state()
|
||||
|
||||
|
||||
@@ -234,7 +235,7 @@ class DemoMusicPlayer(AbstractDemoPlayer):
|
||||
""" Flags of media commands that are supported. """
|
||||
support = MUSIC_PLAYER_SUPPORT
|
||||
|
||||
if self._cur_track > 1:
|
||||
if self._cur_track > 0:
|
||||
support |= SUPPORT_PREVIOUS_TRACK
|
||||
|
||||
if self._cur_track < len(self.tracks)-1:
|
||||
|
||||
@@ -24,7 +24,6 @@ SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Denon platform. """
|
||||
if not config.get(CONF_HOST):
|
||||
@@ -48,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class DenonDevice(MediaPlayerDevice):
|
||||
""" Represents a Denon device. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
|
||||
def __init__(self, name, host):
|
||||
self._name = name
|
||||
@@ -145,10 +144,6 @@ class DenonDevice(MediaPlayerDevice):
|
||||
""" mute (true) or unmute (false) media player. """
|
||||
self.telnet_command("MU" + ("ON" if mute else "OFF"))
|
||||
|
||||
def media_play_pause(self):
|
||||
""" media_play_pause media player. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def media_play(self):
|
||||
""" media_play media player. """
|
||||
self.telnet_command("NS9A")
|
||||
@@ -164,9 +159,6 @@ class DenonDevice(MediaPlayerDevice):
|
||||
def media_previous_track(self):
|
||||
self.telnet_command("NS9E")
|
||||
|
||||
def media_seek(self, position):
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_on(self):
|
||||
""" turn the media player on. """
|
||||
self.telnet_command("PWON")
|
||||
|
||||
@@ -105,6 +105,8 @@ class FireTV(object):
|
||||
class FireTVDevice(MediaPlayerDevice):
|
||||
""" Represents an Amazon Fire TV device on the network. """
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, host, device, name):
|
||||
self._firetv = FireTV(host, device)
|
||||
self._name = name
|
||||
@@ -176,15 +178,3 @@ class FireTVDevice(MediaPlayerDevice):
|
||||
def media_next_track(self):
|
||||
""" Send next track command (results in fast-forward). """
|
||||
self._firetv.action('media_next')
|
||||
|
||||
def media_seek(self, position):
|
||||
raise NotImplementedError()
|
||||
|
||||
def mute_volume(self, mute):
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_youtube(self, media_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -14,8 +14,7 @@ from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_PAUSE,
|
||||
SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON,
|
||||
SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA,
|
||||
ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS)
|
||||
SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON)
|
||||
|
||||
@@ -40,7 +39,10 @@ class Itunes(object):
|
||||
@property
|
||||
def _base_url(self):
|
||||
""" Returns the base url for endpoints. """
|
||||
return self.host + ":" + str(self.port)
|
||||
if self.port:
|
||||
return self.host + ":" + str(self.port)
|
||||
else:
|
||||
return self.host
|
||||
|
||||
def _request(self, method, path, params=None):
|
||||
""" Makes the actual request and returns the parsed response. """
|
||||
@@ -380,6 +382,14 @@ class AirPlayDevice(MediaPlayerDevice):
|
||||
""" Returns the name of the device. """
|
||||
return self.device_name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
""" Icon to use in the frontend, if any. """
|
||||
if self.selected is True:
|
||||
return "mdi:volume-high"
|
||||
else:
|
||||
return "mdi:volume-off"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
@@ -405,23 +415,6 @@ class AirPlayDevice(MediaPlayerDevice):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_AIRPLAY
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
state_attr = {}
|
||||
state_attr[ATTR_SUPPORTED_MEDIA_COMMANDS] = SUPPORT_AIRPLAY
|
||||
|
||||
if self.state == STATE_OFF:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = \
|
||||
('https://cloud.githubusercontent.com/assets/260/9833073'
|
||||
'/6eb5c906-5958-11e5-9b4a-472cdf36be16.png')
|
||||
else:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = \
|
||||
('https://cloud.githubusercontent.com/assets/260/9833072'
|
||||
'/6eb13cce-5958-11e5-996f-e2aaefbc9a24.png')
|
||||
|
||||
return state_attr
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
""" set volume level, range 0..1. """
|
||||
volume = int(volume * 100)
|
||||
|
||||
@@ -22,43 +22,41 @@ SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the kodi platform. """
|
||||
|
||||
url = '{}:{}'.format(config.get('host'), config.get('port', '8080'))
|
||||
|
||||
jsonrpc_url = config.get('url') # deprecated
|
||||
if jsonrpc_url:
|
||||
url = jsonrpc_url.rstrip('/jsonrpc')
|
||||
|
||||
add_devices([
|
||||
KodiDevice(
|
||||
config.get('name', 'Kodi'),
|
||||
config.get('url'),
|
||||
url,
|
||||
auth=(
|
||||
config.get('user', ''),
|
||||
config.get('password', ''))),
|
||||
])
|
||||
|
||||
|
||||
def _get_image_url(kodi_url):
|
||||
""" Helper function that parses the thumbnail URLs used by Kodi. """
|
||||
url_components = urllib.parse.urlparse(kodi_url)
|
||||
|
||||
if url_components.scheme == 'image':
|
||||
return urllib.parse.unquote(url_components.netloc)
|
||||
|
||||
|
||||
class KodiDevice(MediaPlayerDevice):
|
||||
""" Represents a XBMC/Kodi device. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
|
||||
def __init__(self, name, url, auth=None):
|
||||
import jsonrpc_requests
|
||||
self._name = name
|
||||
self._url = url
|
||||
self._server = jsonrpc_requests.Server(url, auth=auth)
|
||||
self._server = jsonrpc_requests.Server(
|
||||
'{}/jsonrpc'.format(self._url),
|
||||
auth=auth)
|
||||
self._players = None
|
||||
self._properties = None
|
||||
self._item = None
|
||||
self._app_properties = None
|
||||
|
||||
self.update()
|
||||
|
||||
@property
|
||||
@@ -72,7 +70,8 @@ class KodiDevice(MediaPlayerDevice):
|
||||
try:
|
||||
return self._server.Player.GetActivePlayers()
|
||||
except jsonrpc_requests.jsonrpc.TransportError:
|
||||
_LOGGER.exception('Unable to fetch kodi data')
|
||||
_LOGGER.warning('Unable to fetch kodi data')
|
||||
_LOGGER.debug('Unable to fetch kodi data', exc_info=True)
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -155,7 +154,16 @@ class KodiDevice(MediaPlayerDevice):
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
if self._item is not None:
|
||||
return _get_image_url(self._item['thumbnail'])
|
||||
return self._get_image_url()
|
||||
|
||||
def _get_image_url(self):
|
||||
""" Helper function that parses the thumbnail URLs used by Kodi. """
|
||||
url_components = urllib.parse.urlparse(self._item['thumbnail'])
|
||||
|
||||
if url_components.scheme == 'image':
|
||||
return '{}/image/{}'.format(
|
||||
self._url,
|
||||
urllib.parse.quote_plus(self._item['thumbnail']))
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
@@ -262,11 +270,3 @@ class KodiDevice(MediaPlayerDevice):
|
||||
self._server.Player.Seek(players[0]['playerid'], time)
|
||||
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_on(self):
|
||||
""" turn the media player on. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_youtube(self, media_id):
|
||||
""" Plays a YouTube media. """
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -9,11 +9,6 @@ https://home-assistant.io/components/media_player.mpd/
|
||||
import logging
|
||||
import socket
|
||||
|
||||
try:
|
||||
import mpd
|
||||
except ImportError:
|
||||
mpd = None
|
||||
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
|
||||
@@ -40,10 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
location = config.get('location', 'MPD')
|
||||
password = config.get('password', None)
|
||||
|
||||
global mpd # pylint: disable=invalid-name
|
||||
if mpd is None:
|
||||
import mpd as mpd_
|
||||
mpd = mpd_
|
||||
import mpd
|
||||
|
||||
# pylint: disable=no-member
|
||||
try:
|
||||
@@ -82,6 +74,8 @@ class MpdDevice(MediaPlayerDevice):
|
||||
# pylint: disable=no-member, abstract-method
|
||||
|
||||
def __init__(self, server, port, location, password):
|
||||
import mpd
|
||||
|
||||
self.server = server
|
||||
self.port = port
|
||||
self._name = location
|
||||
@@ -95,6 +89,7 @@ class MpdDevice(MediaPlayerDevice):
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
import mpd
|
||||
try:
|
||||
self.status = self.client.status()
|
||||
self.currentsong = self.client.currentsong()
|
||||
@@ -142,10 +137,14 @@ class MpdDevice(MediaPlayerDevice):
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
name = self.currentsong.get('name', None)
|
||||
title = self.currentsong['title']
|
||||
title = self.currentsong.get('title', None)
|
||||
|
||||
if name is None:
|
||||
if name is None and title is None:
|
||||
return "None"
|
||||
elif name is None:
|
||||
return title
|
||||
elif title is None:
|
||||
return name
|
||||
else:
|
||||
return '{}: {}'.format(name, title)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
|
||||
|
||||
def config_from_file(filename, config=None):
|
||||
''' Small configuration file management function'''
|
||||
""" Small configuration file management function. """
|
||||
if config:
|
||||
# We're writing configuration
|
||||
try:
|
||||
@@ -59,7 +59,7 @@ def config_from_file(filename, config=None):
|
||||
return {}
|
||||
|
||||
|
||||
# pylint: disable=abstract-method, unused-argument
|
||||
# pylint: disable=abstract-method
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Sets up the plex platform. """
|
||||
|
||||
@@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup_plexserver(host, token, hass, add_devices_callback):
|
||||
''' Setup a plexserver based on host parameter'''
|
||||
""" Setup a plexserver based on host parameter. """
|
||||
import plexapi.server
|
||||
import plexapi.exceptions
|
||||
|
||||
@@ -112,7 +112,7 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
||||
{host: {'token': token}}):
|
||||
_LOGGER.error('failed to save config file')
|
||||
|
||||
_LOGGER.info('Connected to: htts://%s', host)
|
||||
_LOGGER.info('Connected to: http://%s', host)
|
||||
|
||||
plex_clients = {}
|
||||
plex_sessions = {}
|
||||
|
||||
170
homeassistant/components/media_player/samsungtv.py
Normal file
170
homeassistant/components/media_player/samsungtv.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
homeassistant.components.media_player.denon
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides an interface to Samsung TV with a Laninterface.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.samsungtv/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_VOLUME_STEP,
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_TURN_OFF,
|
||||
DOMAIN)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, STATE_OFF,
|
||||
STATE_ON, STATE_UNKNOWN)
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
CONF_PORT = "port"
|
||||
CONF_TIMEOUT = "timeout"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['samsungctl==0.5.1']
|
||||
|
||||
SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Samsung TV platform. """
|
||||
|
||||
# Validate that all required config options are given
|
||||
if not validate_config({DOMAIN: config}, {DOMAIN: [CONF_HOST]}, _LOGGER):
|
||||
return False
|
||||
|
||||
# Default the entity_name to 'Samsung TV Remote'
|
||||
name = config.get(CONF_NAME, 'Samsung TV Remote')
|
||||
|
||||
# Generate a config for the Samsung lib
|
||||
remote_config = {
|
||||
"name": "HomeAssistant",
|
||||
"description": config.get(CONF_NAME, ''),
|
||||
"id": "ha.component.samsung",
|
||||
"port": config.get(CONF_PORT, 55000),
|
||||
"host": config.get(CONF_HOST),
|
||||
"timeout": config.get(CONF_TIMEOUT, 0),
|
||||
}
|
||||
|
||||
add_devices([SamsungTVDevice(name, remote_config)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class SamsungTVDevice(MediaPlayerDevice):
|
||||
""" Represents a Samsung TV. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
def __init__(self, name, config):
|
||||
from samsungctl import Remote
|
||||
# Save a reference to the imported class
|
||||
self._remote_class = Remote
|
||||
self._name = name
|
||||
# Assume that the TV is not muted
|
||||
self._muted = False
|
||||
# Assume that the TV is in Play mode
|
||||
self._playing = True
|
||||
self._state = STATE_UNKNOWN
|
||||
self._remote = None
|
||||
self._config = config
|
||||
|
||||
def update(self):
|
||||
# Send an empty key to see if we are still connected
|
||||
return self.send_key('KEY_POWER')
|
||||
|
||||
def get_remote(self):
|
||||
""" Creates or Returns a remote control instance """
|
||||
|
||||
if self._remote is None:
|
||||
# We need to create a new instance to reconnect.
|
||||
self._remote = self._remote_class(self._config)
|
||||
|
||||
return self._remote
|
||||
|
||||
def send_key(self, key):
|
||||
""" Sends a key to the tv and handles exceptions """
|
||||
try:
|
||||
self.get_remote().control(key)
|
||||
self._state = STATE_ON
|
||||
except (self._remote_class.UnhandledResponse,
|
||||
self._remote_class.AccessDenied, BrokenPipeError):
|
||||
# We got a response so it's on.
|
||||
# BrokenPipe can occur when the commands is sent to fast
|
||||
self._state = STATE_ON
|
||||
self._remote = None
|
||||
return False
|
||||
except (self._remote_class.ConnectionClosed, socket.timeout,
|
||||
TimeoutError, OSError):
|
||||
self._state = STATE_OFF
|
||||
self._remote = None
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
""" Boolean if volume is currently muted. """
|
||||
return self._muted
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_SAMSUNGTV
|
||||
|
||||
def turn_off(self):
|
||||
""" turn_off media player. """
|
||||
self.send_key("KEY_POWEROFF")
|
||||
|
||||
def volume_up(self):
|
||||
""" volume_up media player. """
|
||||
self.send_key("KEY_VOLUP")
|
||||
|
||||
def volume_down(self):
|
||||
""" volume_down media player. """
|
||||
self.send_key("KEY_VOLDOWN")
|
||||
|
||||
def mute_volume(self, mute):
|
||||
self.send_key("KEY_MUTE")
|
||||
|
||||
def media_play_pause(self):
|
||||
""" Simulate play pause media player. """
|
||||
if self._playing:
|
||||
self.media_pause()
|
||||
else:
|
||||
self.media_play()
|
||||
|
||||
def media_play(self):
|
||||
""" media_play media player. """
|
||||
self._playing = True
|
||||
self.send_key("KEY_PLAY")
|
||||
|
||||
def media_pause(self):
|
||||
""" media_pause media player. """
|
||||
self._playing = False
|
||||
self.send_key("KEY_PAUSE")
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command. """
|
||||
self.send_key("KEY_FF")
|
||||
|
||||
def media_previous_track(self):
|
||||
self.send_key("KEY_REWIND")
|
||||
|
||||
def turn_on(self):
|
||||
""" turn the media player on. """
|
||||
self.send_key("KEY_POWERON")
|
||||
85
homeassistant/components/media_player/snapcast.py
Normal file
85
homeassistant/components/media_player/snapcast.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
homeassistant.components.media_player.snapcast
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides functionality to interact with Snapcast clients.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.snapcast/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE)
|
||||
|
||||
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
||||
DOMAIN = 'snapcast'
|
||||
REQUIREMENTS = ['snapcast==1.1.1']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Snapcast platform. """
|
||||
import snapcast.control
|
||||
host = config.get('host')
|
||||
port = config.get('port', snapcast.control.CONTROL_PORT)
|
||||
if not host:
|
||||
_LOGGER.error('No snapserver host specified')
|
||||
return
|
||||
try:
|
||||
server = snapcast.control.Snapserver(host, port)
|
||||
except socket.gaierror:
|
||||
_LOGGER.error('Could not connect to Snapcast server at %s:%d',
|
||||
host, port)
|
||||
return
|
||||
add_devices([SnapcastDevice(client) for client in server.clients])
|
||||
|
||||
|
||||
class SnapcastDevice(MediaPlayerDevice):
|
||||
""" Represents a Snapcast client device. """
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Device name. """
|
||||
return self._client.identifier
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
""" Volume level. """
|
||||
return self._client.volume / 100
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
""" Volume muted. """
|
||||
return self._client.muted
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_SNAPCAST
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" State of the player. """
|
||||
if self._client.connected:
|
||||
return STATE_ON
|
||||
return STATE_OFF
|
||||
|
||||
def mute_volume(self, mute):
|
||||
""" Mute status. """
|
||||
self._client.muted = mute
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
""" Volume level. """
|
||||
self._client.volume = round(volume * 100)
|
||||
@@ -38,12 +38,23 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Sonos platform. """
|
||||
import soco
|
||||
import socket
|
||||
|
||||
if discovery_info:
|
||||
add_devices([SonosDevice(hass, soco.SoCo(discovery_info))])
|
||||
return True
|
||||
|
||||
players = soco.discover()
|
||||
players = None
|
||||
hosts = config.get('hosts', None)
|
||||
if hosts:
|
||||
players = []
|
||||
for host in hosts.split(","):
|
||||
host = socket.gethostbyname(host)
|
||||
players.append(soco.SoCo(host))
|
||||
|
||||
if not players:
|
||||
players = soco.discover(interface_addr=config.get('interface_addr',
|
||||
None))
|
||||
|
||||
if not players:
|
||||
_LOGGER.warning('No Sonos speakers found.')
|
||||
|
||||
@@ -22,12 +22,11 @@ from homeassistant.const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK |\
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the squeezebox platform. """
|
||||
if not config.get(CONF_HOST):
|
||||
@@ -138,7 +137,7 @@ class LogitechMediaServer(object):
|
||||
class SqueezeBoxDevice(MediaPlayerDevice):
|
||||
""" Represents a SqueezeBox device. """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-arguments, abstract-method
|
||||
def __init__(self, lms, player_id):
|
||||
super(SqueezeBoxDevice, self).__init__()
|
||||
self._lms = lms
|
||||
@@ -201,13 +200,20 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
if 'artwork_url' in self._status:
|
||||
return self._status['artwork_url']
|
||||
return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\
|
||||
.format(
|
||||
server=self._lms.host,
|
||||
port=self._lms.http_port,
|
||||
media_url = self._status['artwork_url']
|
||||
elif 'id' in self._status:
|
||||
media_url = ('/music/{track_id}/cover.jpg').format(
|
||||
track_id=self._status['id'])
|
||||
else:
|
||||
media_url = ('/music/current/cover.jpg?player={player}').format(
|
||||
player=self._id)
|
||||
|
||||
base_url = 'http://{server}:{port}/'.format(
|
||||
server=self._lms.host,
|
||||
port=self._lms.http_port)
|
||||
|
||||
return urllib.parse.urljoin(base_url, media_url)
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
@@ -285,7 +291,3 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
||||
""" turn the media player on. """
|
||||
self._lms.query(self._id, 'power', '1')
|
||||
self.update_ha_state()
|
||||
|
||||
def play_youtube(self, media_id):
|
||||
""" Plays a YouTube media. """
|
||||
raise NotImplementedError()
|
||||
|
||||
424
homeassistant/components/media_player/universal.py
Normal file
424
homeassistant/components/media_player/universal.py
Normal file
@@ -0,0 +1,424 @@
|
||||
"""
|
||||
homeassistant.components.media_player.universal
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Combines multiple media players into one for a universal controller.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.universal/
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error
|
||||
from copy import copy
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_ON, STATE_OFF, CONF_NAME,
|
||||
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE,
|
||||
SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, DOMAIN,
|
||||
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_MUTED,
|
||||
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION,
|
||||
ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_ALBUM_NAME,
|
||||
ATTR_MEDIA_TRACK, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_ALBUM_ARTIST,
|
||||
ATTR_MEDIA_SEASON, ATTR_MEDIA_EPISODE, ATTR_MEDIA_CHANNEL,
|
||||
ATTR_MEDIA_PLAYLIST, ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_SEEK_POSITION)
|
||||
|
||||
ATTR_ACTIVE_CHILD = 'active_child'
|
||||
|
||||
CONF_ATTRS = 'attributes'
|
||||
CONF_CHILDREN = 'children'
|
||||
CONF_COMMANDS = 'commands'
|
||||
CONF_PLATFORM = 'platform'
|
||||
CONF_SERVICE = 'service'
|
||||
CONF_SERVICE_DATA = 'service_data'
|
||||
CONF_STATE = 'state'
|
||||
|
||||
OFF_STATES = [STATE_IDLE, STATE_OFF]
|
||||
REQUIREMENTS = []
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" sets up the universal media players """
|
||||
if not validate_config(config):
|
||||
return
|
||||
|
||||
player = UniversalMediaPlayer(hass,
|
||||
config[CONF_NAME],
|
||||
config[CONF_CHILDREN],
|
||||
config[CONF_COMMANDS],
|
||||
config[CONF_ATTRS])
|
||||
|
||||
add_devices([player])
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
""" validate universal media player configuration """
|
||||
del config[CONF_PLATFORM]
|
||||
|
||||
# validate name
|
||||
if CONF_NAME not in config:
|
||||
_LOGGER.error('Universal Media Player configuration requires name')
|
||||
return False
|
||||
|
||||
validate_children(config)
|
||||
validate_commands(config)
|
||||
validate_attributes(config)
|
||||
|
||||
del_keys = []
|
||||
for key in config:
|
||||
if key not in [CONF_NAME, CONF_CHILDREN, CONF_COMMANDS, CONF_ATTRS]:
|
||||
_LOGGER.warning(
|
||||
'Universal Media Player (%s) unrecognized parameter %s',
|
||||
config[CONF_NAME], key)
|
||||
del_keys.append(key)
|
||||
for key in del_keys:
|
||||
del config[key]
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_children(config):
|
||||
""" validate children """
|
||||
if CONF_CHILDREN not in config:
|
||||
_LOGGER.info(
|
||||
'No children under Universal Media Player (%s)', config[CONF_NAME])
|
||||
config[CONF_CHILDREN] = []
|
||||
elif not isinstance(config[CONF_CHILDREN], list):
|
||||
_LOGGER.warning(
|
||||
'Universal Media Player (%s) children not list in config. '
|
||||
'They will be ignored.',
|
||||
config[CONF_NAME])
|
||||
config[CONF_CHILDREN] = []
|
||||
|
||||
|
||||
def validate_commands(config):
|
||||
""" validate commands """
|
||||
if CONF_COMMANDS not in config:
|
||||
config[CONF_COMMANDS] = {}
|
||||
elif not isinstance(config[CONF_COMMANDS], dict):
|
||||
_LOGGER.warning(
|
||||
'Universal Media Player (%s) specified commands not dict in '
|
||||
'config. They will be ignored.',
|
||||
config[CONF_NAME])
|
||||
config[CONF_COMMANDS] = {}
|
||||
|
||||
|
||||
def validate_attributes(config):
|
||||
""" validate attributes """
|
||||
if CONF_ATTRS not in config:
|
||||
config[CONF_ATTRS] = {}
|
||||
elif not isinstance(config[CONF_ATTRS], dict):
|
||||
_LOGGER.warning(
|
||||
'Universal Media Player (%s) specified attributes '
|
||||
'not dict in config. They will be ignored.',
|
||||
config[CONF_NAME])
|
||||
config[CONF_ATTRS] = {}
|
||||
|
||||
for key, val in config[CONF_ATTRS].items():
|
||||
attr = val.split('|', 1)
|
||||
if len(attr) == 1:
|
||||
attr.append(None)
|
||||
config[CONF_ATTRS][key] = attr
|
||||
|
||||
|
||||
class UniversalMediaPlayer(MediaPlayerDevice):
|
||||
""" Represents a universal media player in HA """
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, hass, name, children, commands, attributes):
|
||||
# pylint: disable=too-many-arguments
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._children = children
|
||||
self._cmds = commands
|
||||
self._attrs = attributes
|
||||
self._child_state = None
|
||||
|
||||
def on_dependency_update(*_):
|
||||
""" update ha state when dependencies update """
|
||||
self.update_ha_state(True)
|
||||
|
||||
depend = copy(children)
|
||||
for entity in attributes.values():
|
||||
depend.append(entity[0])
|
||||
|
||||
track_state_change(hass, depend, on_dependency_update)
|
||||
|
||||
def _entity_lkp(self, entity_id, state_attr=None):
|
||||
""" Looks up an entity state from hass """
|
||||
state_obj = self.hass.states.get(entity_id)
|
||||
|
||||
if state_obj is None:
|
||||
return
|
||||
|
||||
if state_attr:
|
||||
return state_obj.attributes.get(state_attr)
|
||||
return state_obj.state
|
||||
|
||||
def _override_or_child_attr(self, attr_name):
|
||||
""" returns either the override or the active child for attr_name """
|
||||
if attr_name in self._attrs:
|
||||
return self._entity_lkp(self._attrs[attr_name][0],
|
||||
self._attrs[attr_name][1])
|
||||
|
||||
return self._child_attr(attr_name)
|
||||
|
||||
def _child_attr(self, attr_name):
|
||||
""" returns the active child's attr """
|
||||
active_child = self._child_state
|
||||
return active_child.attributes.get(attr_name) if active_child else None
|
||||
|
||||
def _call_service(self, service_name, service_data=None,
|
||||
allow_override=False):
|
||||
""" calls either a specified or active child's service """
|
||||
if allow_override and service_name in self._cmds:
|
||||
call_from_config(
|
||||
self.hass, self._cmds[service_name], blocking=True)
|
||||
return
|
||||
|
||||
if service_data is None:
|
||||
service_data = {}
|
||||
|
||||
active_child = self._child_state
|
||||
service_data[ATTR_ENTITY_ID] = active_child.entity_id
|
||||
|
||||
self.hass.services.call(DOMAIN, service_name, service_data,
|
||||
blocking=True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Indicates whether HA should poll for updates """
|
||||
return False
|
||||
|
||||
@property
|
||||
def master_state(self):
|
||||
""" gets the master state from entity or none """
|
||||
if CONF_STATE in self._attrs:
|
||||
master_state = self._entity_lkp(self._attrs[CONF_STATE][0],
|
||||
self._attrs[CONF_STATE][1])
|
||||
return master_state if master_state else STATE_OFF
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" name of universal player """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""
|
||||
Current state of media player
|
||||
|
||||
Off if master state is off
|
||||
ELSE Status of first active child
|
||||
ELSE master state or off
|
||||
"""
|
||||
master_state = self.master_state # avoid multiple lookups
|
||||
if master_state == STATE_OFF:
|
||||
return STATE_OFF
|
||||
|
||||
active_child = self._child_state
|
||||
if active_child:
|
||||
return active_child.state
|
||||
|
||||
return master_state if master_state else STATE_OFF
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
""" Volume level of entity specified in attributes or active child """
|
||||
return self._child_attr(ATTR_MEDIA_VOLUME_LEVEL)
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
""" boolean if volume is muted """
|
||||
return self._override_or_child_attr(ATTR_MEDIA_VOLUME_MUTED) \
|
||||
in [True, STATE_ON]
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
return self._child_attr(ATTR_MEDIA_CONTENT_ID)
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
""" Content type of current playing media. """
|
||||
return self._child_attr(ATTR_MEDIA_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
""" Duration of current playing media in seconds. """
|
||||
return self._child_attr(ATTR_MEDIA_DURATION)
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
return self._child_attr(ATTR_ENTITY_PICTURE)
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
return self._child_attr(ATTR_MEDIA_TITLE)
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
""" Artist of current playing media. (Music track only) """
|
||||
return self._child_attr(ATTR_MEDIA_ARTIST)
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
""" Album name of current playing media. (Music track only) """
|
||||
return self._child_attr(ATTR_MEDIA_ALBUM_NAME)
|
||||
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
""" Album arist of current playing media. (Music track only) """
|
||||
return self._child_attr(ATTR_MEDIA_ALBUM_ARTIST)
|
||||
|
||||
@property
|
||||
def media_track(self):
|
||||
""" Track number of current playing media. (Music track only) """
|
||||
return self._child_attr(ATTR_MEDIA_TRACK)
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
""" Series title of current playing media. (TV Show only)"""
|
||||
return self._child_attr(ATTR_MEDIA_SERIES_TITLE)
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
""" Season of current playing media. (TV Show only) """
|
||||
return self._child_attr(ATTR_MEDIA_SEASON)
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
""" Episode of current playing media. (TV Show only) """
|
||||
return self._child_attr(ATTR_MEDIA_EPISODE)
|
||||
|
||||
@property
|
||||
def media_channel(self):
|
||||
""" Channel currently playing. """
|
||||
return self._child_attr(ATTR_MEDIA_CHANNEL)
|
||||
|
||||
@property
|
||||
def media_playlist(self):
|
||||
""" Title of Playlist currently playing. """
|
||||
return self._child_attr(ATTR_MEDIA_PLAYLIST)
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
""" ID of the current running app. """
|
||||
return self._child_attr(ATTR_APP_ID)
|
||||
|
||||
@property
|
||||
def app_name(self):
|
||||
""" Name of the current running app. """
|
||||
return self._child_attr(ATTR_APP_NAME)
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
flags = self._child_attr(ATTR_SUPPORTED_MEDIA_COMMANDS) or 0
|
||||
|
||||
if SERVICE_TURN_ON in self._cmds:
|
||||
flags |= SUPPORT_TURN_ON
|
||||
if SERVICE_TURN_OFF in self._cmds:
|
||||
flags |= SUPPORT_TURN_OFF
|
||||
|
||||
if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP,
|
||||
SERVICE_VOLUME_DOWN]]):
|
||||
flags |= SUPPORT_VOLUME_STEP
|
||||
flags &= ~SUPPORT_VOLUME_SET
|
||||
|
||||
if SERVICE_VOLUME_MUTE in self._cmds and \
|
||||
ATTR_MEDIA_VOLUME_MUTED in self._attrs:
|
||||
flags |= SUPPORT_VOLUME_MUTE
|
||||
|
||||
return flags
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Extra attributes a device wants to expose. """
|
||||
active_child = self._child_state
|
||||
return {ATTR_ACTIVE_CHILD: active_child.entity_id} \
|
||||
if active_child else {}
|
||||
|
||||
def turn_on(self):
|
||||
""" turn the media player on. """
|
||||
self._call_service(SERVICE_TURN_ON, allow_override=True)
|
||||
|
||||
def turn_off(self):
|
||||
""" turn the media player off. """
|
||||
self._call_service(SERVICE_TURN_OFF, allow_override=True)
|
||||
|
||||
def mute_volume(self, is_volume_muted):
|
||||
""" mute the volume. """
|
||||
data = {ATTR_MEDIA_VOLUME_MUTED: is_volume_muted}
|
||||
self._call_service(SERVICE_VOLUME_MUTE, data, allow_override=True)
|
||||
|
||||
def set_volume_level(self, volume_level):
|
||||
""" set volume level, range 0..1. """
|
||||
data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level}
|
||||
self._call_service(SERVICE_VOLUME_SET, data)
|
||||
|
||||
def media_play(self):
|
||||
""" Send play commmand. """
|
||||
self._call_service(SERVICE_MEDIA_PLAY)
|
||||
|
||||
def media_pause(self):
|
||||
""" Send pause command. """
|
||||
self._call_service(SERVICE_MEDIA_PAUSE)
|
||||
|
||||
def media_previous_track(self):
|
||||
""" Send previous track command. """
|
||||
self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK)
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command. """
|
||||
self._call_service(SERVICE_MEDIA_NEXT_TRACK)
|
||||
|
||||
def media_seek(self, position):
|
||||
""" Send seek command. """
|
||||
data = {ATTR_MEDIA_SEEK_POSITION: position}
|
||||
self._call_service(SERVICE_MEDIA_SEEK, data)
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
""" Plays a piece of media. """
|
||||
data = {'media_type': media_type, 'media_id': media_id}
|
||||
self._call_service(SERVICE_PLAY_MEDIA, data)
|
||||
|
||||
def volume_up(self):
|
||||
""" volume_up media player. """
|
||||
self._call_service(SERVICE_VOLUME_UP, allow_override=True)
|
||||
|
||||
def volume_down(self):
|
||||
""" volume_down media player. """
|
||||
self._call_service(SERVICE_VOLUME_DOWN, allow_override=True)
|
||||
|
||||
def media_play_pause(self):
|
||||
""" media_play_pause media player. """
|
||||
self._call_service(SERVICE_MEDIA_PLAY_PAUSE)
|
||||
|
||||
def update(self):
|
||||
""" event to trigger a state update in HA """
|
||||
for child_name in self._children:
|
||||
child_state = self.hass.states.get(child_name)
|
||||
if child_state and child_state.state not in OFF_STATES:
|
||||
self._child_state = child_state
|
||||
return
|
||||
self._child_state = None
|
||||
@@ -12,8 +12,10 @@ import socket
|
||||
import time
|
||||
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util import template
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
@@ -24,13 +26,8 @@ DOMAIN = "mqtt"
|
||||
|
||||
MQTT_CLIENT = None
|
||||
|
||||
DEFAULT_PORT = 1883
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_QOS = 0
|
||||
DEFAULT_RETAIN = False
|
||||
|
||||
SERVICE_PUBLISH = 'publish'
|
||||
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
|
||||
EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received'
|
||||
|
||||
REQUIREMENTS = ['paho-mqtt==1.1']
|
||||
|
||||
@@ -41,34 +38,54 @@ CONF_KEEPALIVE = 'keepalive'
|
||||
CONF_USERNAME = 'username'
|
||||
CONF_PASSWORD = 'password'
|
||||
CONF_CERTIFICATE = 'certificate'
|
||||
CONF_PROTOCOL = 'protocol'
|
||||
|
||||
PROTOCOL_31 = '3.1'
|
||||
PROTOCOL_311 = '3.1.1'
|
||||
|
||||
DEFAULT_PORT = 1883
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_QOS = 0
|
||||
DEFAULT_RETAIN = False
|
||||
DEFAULT_PROTOCOL = PROTOCOL_311
|
||||
|
||||
ATTR_TOPIC = 'topic'
|
||||
ATTR_PAYLOAD = 'payload'
|
||||
ATTR_PAYLOAD_TEMPLATE = 'payload_template'
|
||||
ATTR_QOS = 'qos'
|
||||
ATTR_RETAIN = 'retain'
|
||||
|
||||
MAX_RECONNECT_WAIT = 300 # seconds
|
||||
|
||||
|
||||
def publish(hass, topic, payload, qos=None, retain=None):
|
||||
""" Send an MQTT message. """
|
||||
data = {
|
||||
ATTR_TOPIC: topic,
|
||||
ATTR_PAYLOAD: payload,
|
||||
}
|
||||
def _build_publish_data(topic, qos, retain):
|
||||
"""Build the arguments for the publish service without the payload."""
|
||||
data = {ATTR_TOPIC: topic}
|
||||
if qos is not None:
|
||||
data[ATTR_QOS] = qos
|
||||
|
||||
if retain is not None:
|
||||
data[ATTR_RETAIN] = retain
|
||||
return data
|
||||
|
||||
|
||||
def publish(hass, topic, payload, qos=None, retain=None):
|
||||
"""Publish message to an MQTT topic."""
|
||||
data = _build_publish_data(topic, qos, retain)
|
||||
data[ATTR_PAYLOAD] = payload
|
||||
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
||||
|
||||
|
||||
def publish_template(hass, topic, payload_template, qos=None, retain=None):
|
||||
"""Publish message to an MQTT topic using a template payload."""
|
||||
data = _build_publish_data(topic, qos, retain)
|
||||
data[ATTR_PAYLOAD_TEMPLATE] = payload_template
|
||||
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
||||
|
||||
|
||||
def subscribe(hass, topic, callback, qos=DEFAULT_QOS):
|
||||
""" Subscribe to a topic. """
|
||||
"""Subscribe to an MQTT topic."""
|
||||
def mqtt_topic_subscriber(event):
|
||||
""" Match subscribed MQTT topic. """
|
||||
"""Match subscribed MQTT topic."""
|
||||
if _match_topic(topic, event.data[ATTR_TOPIC]):
|
||||
callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD],
|
||||
event.data[ATTR_QOS])
|
||||
@@ -78,8 +95,7 @@ def subscribe(hass, topic, callback, qos=DEFAULT_QOS):
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Get the MQTT protocol service. """
|
||||
|
||||
"""Start the MQTT protocol service."""
|
||||
if not validate_config(config, {DOMAIN: ['broker']}, _LOGGER):
|
||||
return False
|
||||
|
||||
@@ -92,6 +108,12 @@ def setup(hass, config):
|
||||
username = util.convert(conf.get(CONF_USERNAME), str)
|
||||
password = util.convert(conf.get(CONF_PASSWORD), str)
|
||||
certificate = util.convert(conf.get(CONF_CERTIFICATE), str)
|
||||
protocol = util.convert(conf.get(CONF_PROTOCOL), str, DEFAULT_PROTOCOL)
|
||||
|
||||
if protocol not in (PROTOCOL_31, PROTOCOL_311):
|
||||
_LOGGER.error('Invalid protocol specified: %s. Allowed values: %s, %s',
|
||||
protocol, PROTOCOL_31, PROTOCOL_311)
|
||||
return False
|
||||
|
||||
# For cloudmqtt.com, secured connection, auto fill in certificate
|
||||
if certificate is None and 19999 < port < 30000 and \
|
||||
@@ -102,7 +124,7 @@ def setup(hass, config):
|
||||
global MQTT_CLIENT
|
||||
try:
|
||||
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
|
||||
password, certificate)
|
||||
password, certificate, protocol)
|
||||
except socket.error:
|
||||
_LOGGER.exception("Can't connect to the broker. "
|
||||
"Please check your settings and the broker "
|
||||
@@ -110,175 +132,207 @@ def setup(hass, config):
|
||||
return False
|
||||
|
||||
def stop_mqtt(event):
|
||||
""" Stop MQTT component. """
|
||||
"""Stop MQTT component."""
|
||||
MQTT_CLIENT.stop()
|
||||
|
||||
def start_mqtt(event):
|
||||
""" Launch MQTT component when Home Assistant starts up. """
|
||||
"""Launch MQTT component when Home Assistant starts up."""
|
||||
MQTT_CLIENT.start()
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_mqtt)
|
||||
|
||||
def publish_service(call):
|
||||
""" Handle MQTT publish service calls. """
|
||||
"""Handle MQTT publish service calls."""
|
||||
msg_topic = call.data.get(ATTR_TOPIC)
|
||||
payload = call.data.get(ATTR_PAYLOAD)
|
||||
payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE)
|
||||
qos = call.data.get(ATTR_QOS, DEFAULT_QOS)
|
||||
retain = call.data.get(ATTR_RETAIN, DEFAULT_RETAIN)
|
||||
if payload is None:
|
||||
if payload_template is None:
|
||||
_LOGGER.error(
|
||||
"You must set either '%s' or '%s' to use this service",
|
||||
ATTR_PAYLOAD, ATTR_PAYLOAD_TEMPLATE)
|
||||
return
|
||||
try:
|
||||
payload = template.render(hass, payload_template)
|
||||
except template.jinja2.TemplateError as exc:
|
||||
_LOGGER.error(
|
||||
"Unable to publish to '%s': rendering payload template of "
|
||||
"'%s' failed because %s.",
|
||||
msg_topic, payload_template, exc)
|
||||
return
|
||||
if msg_topic is None or payload is None:
|
||||
return
|
||||
MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service,
|
||||
descriptions.get(SERVICE_PUBLISH))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class MQTT(object):
|
||||
""" Implements messaging service for MQTT. """
|
||||
"""Home Assistant MQTT client."""
|
||||
|
||||
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
||||
password, certificate):
|
||||
password, certificate, protocol):
|
||||
"""Initialize Home Assistant MQTT client."""
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
self.userdata = {
|
||||
'hass': hass,
|
||||
'topics': {},
|
||||
'progress': {},
|
||||
}
|
||||
self.hass = hass
|
||||
self.topics = {}
|
||||
self.progress = {}
|
||||
|
||||
if protocol == PROTOCOL_31:
|
||||
proto = mqtt.MQTTv31
|
||||
else:
|
||||
proto = mqtt.MQTTv311
|
||||
|
||||
if client_id is None:
|
||||
self._mqttc = mqtt.Client()
|
||||
self._mqttc = mqtt.Client(protocol=proto)
|
||||
else:
|
||||
self._mqttc = mqtt.Client(client_id)
|
||||
|
||||
self._mqttc.user_data_set(self.userdata)
|
||||
self._mqttc = mqtt.Client(client_id, protocol=proto)
|
||||
|
||||
if username is not None:
|
||||
self._mqttc.username_pw_set(username, password)
|
||||
if certificate is not None:
|
||||
self._mqttc.tls_set(certificate)
|
||||
|
||||
self._mqttc.on_subscribe = _mqtt_on_subscribe
|
||||
self._mqttc.on_unsubscribe = _mqtt_on_unsubscribe
|
||||
self._mqttc.on_connect = _mqtt_on_connect
|
||||
self._mqttc.on_disconnect = _mqtt_on_disconnect
|
||||
self._mqttc.on_message = _mqtt_on_message
|
||||
self._mqttc.on_subscribe = self._mqtt_on_subscribe
|
||||
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
|
||||
self._mqttc.on_connect = self._mqtt_on_connect
|
||||
self._mqttc.on_disconnect = self._mqtt_on_disconnect
|
||||
self._mqttc.on_message = self._mqtt_on_message
|
||||
|
||||
self._mqttc.connect(broker, port, keepalive)
|
||||
|
||||
def publish(self, topic, payload, qos, retain):
|
||||
""" Publish a MQTT message. """
|
||||
"""Publish a MQTT message."""
|
||||
self._mqttc.publish(topic, payload, qos, retain)
|
||||
|
||||
def start(self):
|
||||
""" Run the MQTT client. """
|
||||
"""Run the MQTT client."""
|
||||
self._mqttc.loop_start()
|
||||
|
||||
def stop(self):
|
||||
""" Stop the MQTT client. """
|
||||
"""Stop the MQTT client."""
|
||||
self._mqttc.disconnect()
|
||||
self._mqttc.loop_stop()
|
||||
|
||||
def subscribe(self, topic, qos):
|
||||
""" Subscribe to a topic. """
|
||||
if topic in self.userdata['topics']:
|
||||
"""Subscribe to a topic."""
|
||||
assert isinstance(topic, str)
|
||||
|
||||
if topic in self.topics:
|
||||
return
|
||||
result, mid = self._mqttc.subscribe(topic, qos)
|
||||
_raise_on_error(result)
|
||||
self.userdata['progress'][mid] = topic
|
||||
self.userdata['topics'][topic] = None
|
||||
self.progress[mid] = topic
|
||||
self.topics[topic] = None
|
||||
|
||||
def unsubscribe(self, topic):
|
||||
""" Unsubscribe from topic. """
|
||||
"""Unsubscribe from topic."""
|
||||
result, mid = self._mqttc.unsubscribe(topic)
|
||||
_raise_on_error(result)
|
||||
self.userdata['progress'][mid] = topic
|
||||
self.progress[mid] = topic
|
||||
|
||||
def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code):
|
||||
"""On connect callback.
|
||||
|
||||
def _mqtt_on_message(mqttc, userdata, msg):
|
||||
""" Message callback """
|
||||
userdata['hass'].bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
|
||||
ATTR_TOPIC: msg.topic,
|
||||
ATTR_QOS: msg.qos,
|
||||
ATTR_PAYLOAD: msg.payload.decode('utf-8'),
|
||||
})
|
||||
Resubscribe to all topics we were subscribed to.
|
||||
"""
|
||||
if result_code != 0:
|
||||
_LOGGER.error('Unable to connect to the MQTT broker: %s', {
|
||||
1: 'Incorrect protocol version',
|
||||
2: 'Invalid client identifier',
|
||||
3: 'Server unavailable',
|
||||
4: 'Bad username or password',
|
||||
5: 'Not authorised'
|
||||
}.get(result_code, 'Unknown reason'))
|
||||
self._mqttc.disconnect()
|
||||
return
|
||||
|
||||
old_topics = self.topics
|
||||
|
||||
def _mqtt_on_connect(mqttc, userdata, flags, result_code):
|
||||
""" On connect, resubscribe to all topics we were subscribed to. """
|
||||
if result_code != 0:
|
||||
_LOGGER.error('Unable to connect to the MQTT broker: %s', {
|
||||
1: 'Incorrect protocol version',
|
||||
2: 'Invalid client identifier',
|
||||
3: 'Server unavailable',
|
||||
4: 'Bad username or password',
|
||||
5: 'Not authorised'
|
||||
}.get(result_code, 'Unknown reason'))
|
||||
mqttc.disconnect()
|
||||
return
|
||||
self.topics = {key: value for key, value in self.topics.items()
|
||||
if value is None}
|
||||
|
||||
old_topics = userdata['topics']
|
||||
for topic, qos in old_topics.items():
|
||||
# qos is None if we were in process of subscribing
|
||||
if qos is not None:
|
||||
self.subscribe(topic, qos)
|
||||
|
||||
userdata['topics'] = {}
|
||||
userdata['progress'] = {}
|
||||
def _mqtt_on_subscribe(self, _mqttc, _userdata, mid, granted_qos):
|
||||
"""Subscribe successful callback."""
|
||||
topic = self.progress.pop(mid, None)
|
||||
if topic is None:
|
||||
return
|
||||
self.topics[topic] = granted_qos[0]
|
||||
|
||||
for topic, qos in old_topics.items():
|
||||
# qos is None if we were in process of subscribing
|
||||
if qos is not None:
|
||||
mqttc.subscribe(topic, qos)
|
||||
def _mqtt_on_message(self, _mqttc, _userdata, msg):
|
||||
"""Message received callback."""
|
||||
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
|
||||
ATTR_TOPIC: msg.topic,
|
||||
ATTR_QOS: msg.qos,
|
||||
ATTR_PAYLOAD: msg.payload.decode('utf-8'),
|
||||
})
|
||||
|
||||
def _mqtt_on_unsubscribe(self, _mqttc, _userdata, mid, granted_qos):
|
||||
"""Unsubscribe successful callback."""
|
||||
topic = self.progress.pop(mid, None)
|
||||
if topic is None:
|
||||
return
|
||||
self.topics.pop(topic, None)
|
||||
|
||||
def _mqtt_on_subscribe(mqttc, userdata, mid, granted_qos):
|
||||
""" Called when subscribe successful. """
|
||||
topic = userdata['progress'].pop(mid, None)
|
||||
if topic is None:
|
||||
return
|
||||
userdata['topics'][topic] = granted_qos
|
||||
def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code):
|
||||
"""Disconnected callback."""
|
||||
self.progress = {}
|
||||
self.topics = {key: value for key, value in self.topics.items()
|
||||
if value is not None}
|
||||
|
||||
# Remove None values from topic list
|
||||
for key in list(self.topics):
|
||||
if self.topics[key] is None:
|
||||
self.topics.pop(key)
|
||||
|
||||
def _mqtt_on_unsubscribe(mqttc, userdata, mid, granted_qos):
|
||||
""" Called when subscribe successful. """
|
||||
topic = userdata['progress'].pop(mid, None)
|
||||
if topic is None:
|
||||
return
|
||||
userdata['topics'].pop(topic, None)
|
||||
# When disconnected because of calling disconnect()
|
||||
if result_code == 0:
|
||||
return
|
||||
|
||||
tries = 0
|
||||
wait_time = 0
|
||||
|
||||
def _mqtt_on_disconnect(mqttc, userdata, result_code):
|
||||
""" Called when being disconnected. """
|
||||
# When disconnected because of calling disconnect()
|
||||
if result_code == 0:
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
if self._mqttc.reconnect() == 0:
|
||||
_LOGGER.info('Successfully reconnected to the MQTT server')
|
||||
break
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
tries = 0
|
||||
wait_time = 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
if mqttc.reconnect() == 0:
|
||||
_LOGGER.info('Successfully reconnected to the MQTT server')
|
||||
break
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
wait_time = min(2**tries, MAX_RECONNECT_WAIT)
|
||||
_LOGGER.warning(
|
||||
'Disconnected from MQTT (%s). Trying to reconnect in %ss',
|
||||
result_code, wait_time)
|
||||
# It is ok to sleep here as we are in the MQTT thread.
|
||||
time.sleep(wait_time)
|
||||
tries += 1
|
||||
wait_time = min(2**tries, MAX_RECONNECT_WAIT)
|
||||
_LOGGER.warning(
|
||||
'Disconnected from MQTT (%s). Trying to reconnect in %ss',
|
||||
result_code, wait_time)
|
||||
# It is ok to sleep here as we are in the MQTT thread.
|
||||
time.sleep(wait_time)
|
||||
tries += 1
|
||||
|
||||
|
||||
def _raise_on_error(result):
|
||||
""" Raise error if error result. """
|
||||
"""Raise error if error result."""
|
||||
if result != 0:
|
||||
raise HomeAssistantError('Error talking to MQTT: {}'.format(result))
|
||||
|
||||
|
||||
def _match_topic(subscription, topic):
|
||||
""" Returns if topic matches subscription. """
|
||||
"""Test if topic matches subscription."""
|
||||
if subscription.endswith('#'):
|
||||
return (subscription[:-2] == topic or
|
||||
topic.startswith(subscription[:-1]))
|
||||
|
||||
29
homeassistant/components/mqtt/services.yaml
Normal file
29
homeassistant/components/mqtt/services.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
publish:
|
||||
description: Publish a message to an MQTT topic
|
||||
|
||||
fields:
|
||||
topic:
|
||||
description: Topic to publish payload
|
||||
example: /homeassistant/hello
|
||||
|
||||
payload:
|
||||
description: Payload to publish
|
||||
example: This is great
|
||||
|
||||
payload_template:
|
||||
description: Template to render as payload value. Ignored if payload given.
|
||||
example: "{{ states('sensor.temperature') }}"
|
||||
|
||||
qos:
|
||||
description: Quality of Service
|
||||
example: 2
|
||||
values:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
default: 0
|
||||
|
||||
retain:
|
||||
description: If message should have the retain flag set.
|
||||
example: true
|
||||
default: false
|
||||
105
homeassistant/components/mqtt_eventstream.py
Normal file
105
homeassistant/components/mqtt_eventstream.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
homeassistant.components.mqtt_eventstream
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Connect two Home Assistant instances via MQTT..
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/mqtt_eventstream.html
|
||||
"""
|
||||
import json
|
||||
from homeassistant.core import EventOrigin, State
|
||||
from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
|
||||
from homeassistant.components.mqtt import SERVICE_PUBLISH as MQTT_SVC_PUBLISH
|
||||
from homeassistant.const import (
|
||||
ATTR_SERVICE_DATA,
|
||||
MATCH_ALL,
|
||||
EVENT_TIME_CHANGED,
|
||||
EVENT_CALL_SERVICE,
|
||||
EVENT_SERVICE_EXECUTED,
|
||||
EVENT_STATE_CHANGED,
|
||||
)
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.remote import JSONEncoder
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component
|
||||
DOMAIN = "mqtt_eventstream"
|
||||
|
||||
# List of component names (string) your component depends upon
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup th MQTT eventstream component. """
|
||||
mqtt = loader.get_component('mqtt')
|
||||
pub_topic = config[DOMAIN].get('publish_topic', None)
|
||||
sub_topic = config[DOMAIN].get('subscribe_topic', None)
|
||||
|
||||
def _event_publisher(event):
|
||||
""" Handle events by publishing them on the MQTT queue. """
|
||||
if event.origin != EventOrigin.local:
|
||||
return
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
|
||||
# Filter out the events that were triggered by publishing
|
||||
# to the MQTT topic, or you will end up in an infinite loop.
|
||||
if event.event_type == EVENT_CALL_SERVICE:
|
||||
if (
|
||||
event.data.get('domain') == MQTT_DOMAIN and
|
||||
event.data.get('service') == MQTT_SVC_PUBLISH and
|
||||
event.data[ATTR_SERVICE_DATA].get('topic') == pub_topic
|
||||
):
|
||||
return
|
||||
|
||||
# Filter out all the "event service executed" events because they
|
||||
# are only used internally by core as callbacks for blocking
|
||||
# during the interval while a service is being executed.
|
||||
# They will serve no purpose to the external system,
|
||||
# and thus are unnecessary traffic.
|
||||
# And at any rate it would cause an infinite loop to publish them
|
||||
# because publishing to an MQTT topic itself triggers one.
|
||||
if event.event_type == EVENT_SERVICE_EXECUTED:
|
||||
return
|
||||
|
||||
event_info = {'event_type': event.event_type, 'event_data': event.data}
|
||||
msg = json.dumps(event_info, cls=JSONEncoder)
|
||||
mqtt.publish(hass, pub_topic, msg)
|
||||
|
||||
# Only listen for local events if you are going to publish them
|
||||
if pub_topic:
|
||||
hass.bus.listen(MATCH_ALL, _event_publisher)
|
||||
|
||||
# Process events from a remote server that are received on a queue
|
||||
def _event_receiver(topic, payload, qos):
|
||||
"""
|
||||
Receive events published by the other HA instance and fire
|
||||
them on this hass instance.
|
||||
"""
|
||||
event = json.loads(payload)
|
||||
event_type = event.get('event_type')
|
||||
event_data = event.get('event_data')
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
# Copied over from the _handle_api_post_events_event method
|
||||
# of the api component.
|
||||
if event_type == EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
hass.bus.fire(
|
||||
event_type,
|
||||
event_data=event_data,
|
||||
origin=EventOrigin.remote
|
||||
)
|
||||
|
||||
# Only subscribe if you specified a topic
|
||||
if sub_topic:
|
||||
mqtt.subscribe(hass, sub_topic, _event_receiver)
|
||||
|
||||
hass.states.set('{domain}.initialized'.format(domain=DOMAIN), True)
|
||||
# return boolean to indicate that initialization was successful
|
||||
return True
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user