forked from home-assistant/core
Compare commits
486 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37bb626dd2 | ||
|
|
21273de6a1 | ||
|
|
fe271749c2 | ||
|
|
af0253b2eb | ||
|
|
986bcfef21 | ||
|
|
96f19c7205 | ||
|
|
cdc2df012c | ||
|
|
8dd790e745 | ||
|
|
e90e94b667 | ||
|
|
52f40b3370 | ||
|
|
8ed75217e1 | ||
|
|
1e92417804 | ||
|
|
be9cdf51d9 | ||
|
|
0e1a3c0665 | ||
|
|
b77df372d6 | ||
|
|
8c525b3087 | ||
|
|
5359001c04 | ||
|
|
2481cd2012 | ||
|
|
e4ddb00086 | ||
|
|
f9a019ea82 | ||
|
|
417240ee3e | ||
|
|
ffc2541ba5 | ||
|
|
d74dbc35f2 | ||
|
|
1c2224cc5c | ||
|
|
56c66a19f0 | ||
|
|
79da44a6b3 | ||
|
|
d9805160bc | ||
|
|
f463f4d8c6 | ||
|
|
4da8ec0a05 | ||
|
|
fb34f94d9c | ||
|
|
513c2b03c9 | ||
|
|
4dc9ac820f | ||
|
|
619d329a16 | ||
|
|
e2c6f538a8 | ||
|
|
8739991676 | ||
|
|
26b097b860 | ||
|
|
4e8723f345 | ||
|
|
85f30b893e | ||
|
|
6cadb796bc | ||
|
|
9eaa057739 | ||
|
|
b6324b511c | ||
|
|
80a9539f97 | ||
|
|
8c266f9266 | ||
|
|
5043b85c58 | ||
|
|
890c11cc7c | ||
|
|
253c8aee1f | ||
|
|
12e1602a81 | ||
|
|
25a25dde7a | ||
|
|
c0eaf0386c | ||
|
|
6b96bc3859 | ||
|
|
6d94c121a7 | ||
|
|
ae34640a80 | ||
|
|
8832de80bc | ||
|
|
ed3f7d1581 | ||
|
|
062fb7ac4c | ||
|
|
cc293db5ab | ||
|
|
c9c102815a | ||
|
|
5d23afdc9e | ||
|
|
e95b48ca44 | ||
|
|
646c03eea1 | ||
|
|
05ece53ec2 | ||
|
|
b5214af762 | ||
|
|
3630dc7ff3 | ||
|
|
e7fc8a1890 | ||
|
|
2891b0cb2e | ||
|
|
fc44a4ed99 | ||
|
|
444b7c5ee7 | ||
|
|
690760404b | ||
|
|
6a9968ccb9 | ||
|
|
e91ed1f2a4 | ||
|
|
115c59d88c | ||
|
|
97bb252d23 | ||
|
|
20a1a52bd5 | ||
|
|
9e27e05a84 | ||
|
|
67c48736a2 | ||
|
|
35805e51a3 | ||
|
|
6057b41151 | ||
|
|
2374659984 | ||
|
|
2c3195522f | ||
|
|
b3e88d1f8f | ||
|
|
dd7d8d56bb | ||
|
|
df19172e56 | ||
|
|
f060dcc0aa | ||
|
|
5c168ab551 | ||
|
|
fe9b45c964 | ||
|
|
38c189ecf4 | ||
|
|
8e4f0ea5ae | ||
|
|
248d974ded | ||
|
|
2d93285689 | ||
|
|
68390373e5 | ||
|
|
c0f8e6c5c5 | ||
|
|
85d7377beb | ||
|
|
f17cf1d26b | ||
|
|
e50b59a56c | ||
|
|
e43fefa8f6 | ||
|
|
e819678e27 | ||
|
|
9df7302603 | ||
|
|
afe88dfa0f | ||
|
|
027ce2f555 | ||
|
|
63a10233c5 | ||
|
|
acbf45d5f8 | ||
|
|
d13f3eca92 | ||
|
|
fc291dd5ab | ||
|
|
583e57042b | ||
|
|
9d0c2a8dae | ||
|
|
c2ef22bd08 | ||
|
|
2561efe45d | ||
|
|
c191c13f3a | ||
|
|
b1291e572e | ||
|
|
d1416056cd | ||
|
|
7987065ad7 | ||
|
|
fc8940111d | ||
|
|
63c9d59d54 | ||
|
|
61ccbb59ce | ||
|
|
5fabfced38 | ||
|
|
6c39e1ef19 | ||
|
|
632466bb56 | ||
|
|
7784c40f12 | ||
|
|
41fa8cc8f2 | ||
|
|
2a2a106e62 | ||
|
|
45e140149b | ||
|
|
34368a6b69 | ||
|
|
e8f5445acc | ||
|
|
00b9297082 | ||
|
|
2bdad5388b | ||
|
|
29fb65b224 | ||
|
|
560a4ef5eb | ||
|
|
186f8f6996 | ||
|
|
238884dfe2 | ||
|
|
6da08deabf | ||
|
|
e970edbf20 | ||
|
|
7c69941f13 | ||
|
|
179655b6b0 | ||
|
|
6ebff3cda4 | ||
|
|
70eaa5f10e | ||
|
|
485e81db79 | ||
|
|
fc2f41fe8a | ||
|
|
a4b0e8f897 | ||
|
|
3cf99e29be | ||
|
|
5f8eb08cd9 | ||
|
|
d1424714c7 | ||
|
|
74e93e5853 | ||
|
|
bd72f45788 | ||
|
|
845fd532f0 | ||
|
|
46404a84ec | ||
|
|
ebce666264 | ||
|
|
15cf34f45f | ||
|
|
e620479cc8 | ||
|
|
b292a4af3f | ||
|
|
79d71c6727 | ||
|
|
0b850b555f | ||
|
|
176c99f0cd | ||
|
|
42e59b465e | ||
|
|
e8a701ffd0 | ||
|
|
32f58baa85 | ||
|
|
9794336113 | ||
|
|
ed82f23da3 | ||
|
|
48c86e07fa | ||
|
|
76a0763cbc | ||
|
|
f4f36a3662 | ||
|
|
e201bcad14 | ||
|
|
5182f76aea | ||
|
|
53b1c75d81 | ||
|
|
fdc769abf7 | ||
|
|
f57e307c7a | ||
|
|
f9d89a016e | ||
|
|
205f24c070 | ||
|
|
4bf1972393 | ||
|
|
4fa0119245 | ||
|
|
ccde371a9d | ||
|
|
4e7cc110d9 | ||
|
|
05ba78d886 | ||
|
|
ee56e33193 | ||
|
|
106bf467f8 | ||
|
|
56cbfb5f2a | ||
|
|
193188b965 | ||
|
|
4197c9ee85 | ||
|
|
9418c61b25 | ||
|
|
62caea6bfb | ||
|
|
80053ef21b | ||
|
|
bd4304e838 | ||
|
|
c08c8c7996 | ||
|
|
9d39a5ced3 | ||
|
|
816b69c807 | ||
|
|
9f62d5e3cf | ||
|
|
796a3ff49d | ||
|
|
089e1ab6f4 | ||
|
|
5ad715507b | ||
|
|
ead4e44cd6 | ||
|
|
2a4c5466ef | ||
|
|
2ab14bbabc | ||
|
|
28b7a3da32 | ||
|
|
bf26b75d27 | ||
|
|
3ea4691fce | ||
|
|
f27ad76230 | ||
|
|
60053a642c | ||
|
|
d9f5398c56 | ||
|
|
5df985a510 | ||
|
|
789929d445 | ||
|
|
51a65ee8e9 | ||
|
|
222cc4c393 | ||
|
|
ce1a2cc2a6 | ||
|
|
aab7442cc5 | ||
|
|
4f1eab138c | ||
|
|
53df3fadd7 | ||
|
|
41c2bdb4fb | ||
|
|
d16c5f9046 | ||
|
|
29d4dca56a | ||
|
|
9722125234 | ||
|
|
78c302855a | ||
|
|
c1b197419d | ||
|
|
6cce934f72 | ||
|
|
38cb32afd6 | ||
|
|
c96c283293 | ||
|
|
2fb4709a94 | ||
|
|
42f450d4e6 | ||
|
|
6ea866c7f7 | ||
|
|
429b637885 | ||
|
|
f05a8bfa2a | ||
|
|
96e3dfeb53 | ||
|
|
520de0d278 | ||
|
|
2cacfb5477 | ||
|
|
4960892256 | ||
|
|
834d0e489e | ||
|
|
1e1d593ef7 | ||
|
|
8a93cc147a | ||
|
|
628b9bd8d8 | ||
|
|
1bec2c005d | ||
|
|
587948ec06 | ||
|
|
f641a6aad3 | ||
|
|
a1d5daee53 | ||
|
|
8a2134b3a8 | ||
|
|
c06d92900a | ||
|
|
a628112e4c | ||
|
|
6e0efbe35e | ||
|
|
778761ebce | ||
|
|
76a3a4892d | ||
|
|
bef4ae3e35 | ||
|
|
818a52508e | ||
|
|
02f8779de8 | ||
|
|
33f8ca5abc | ||
|
|
3700fce859 | ||
|
|
9d20a53d63 | ||
|
|
1d68777981 | ||
|
|
f5b305c980 | ||
|
|
382f9a8f49 | ||
|
|
778c3bb83d | ||
|
|
e57d0f345e | ||
|
|
ed70fc9322 | ||
|
|
82c7195484 | ||
|
|
9be7763144 | ||
|
|
875edef3f0 | ||
|
|
3de95c068a | ||
|
|
51c5534c2a | ||
|
|
d95b75a10c | ||
|
|
1f25aa74dd | ||
|
|
5986d9ff5b | ||
|
|
eb6fb5549f | ||
|
|
7596ac23fc | ||
|
|
c37883c9a9 | ||
|
|
c6b285c666 | ||
|
|
b1dc48822d | ||
|
|
ff6f5cc116 | ||
|
|
da8be253bc | ||
|
|
2547a235c1 | ||
|
|
fdb698bef0 | ||
|
|
586e54f8bf | ||
|
|
431201cb9b | ||
|
|
9b43388093 | ||
|
|
45620d6892 | ||
|
|
7ed21d90aa | ||
|
|
959a7b2d59 | ||
|
|
ac256d5943 | ||
|
|
0362a76cd6 | ||
|
|
26cb67dec2 | ||
|
|
00244380a8 | ||
|
|
f807a3a890 | ||
|
|
fd6c2598a7 | ||
|
|
79d1a0ab37 | ||
|
|
a787ab6d3c | ||
|
|
8456cd0313 | ||
|
|
fa37d9800e | ||
|
|
80826bc985 | ||
|
|
b00d0a1253 | ||
|
|
f7545fe85c | ||
|
|
c69e9c1d49 | ||
|
|
79b029a680 | ||
|
|
9891320e7c | ||
|
|
64853bae32 | ||
|
|
a7f4bcc410 | ||
|
|
bbb406626b | ||
|
|
c5c594ba7d | ||
|
|
8d83912649 | ||
|
|
2c1f0f3449 | ||
|
|
c85b5561ee | ||
|
|
4cf300a710 | ||
|
|
3bdb7052b8 | ||
|
|
3b5a9e7796 | ||
|
|
5fcb0990c3 | ||
|
|
be5c0b2d92 | ||
|
|
38e02a057d | ||
|
|
fad9e607c3 | ||
|
|
c33b179fb8 | ||
|
|
765560e87a | ||
|
|
f837302194 | ||
|
|
47d8601f30 | ||
|
|
bddb424b0d | ||
|
|
8db4b4f303 | ||
|
|
cc4ec228b5 | ||
|
|
c6e6496000 | ||
|
|
2c9010d661 | ||
|
|
24826c2770 | ||
|
|
c1aaed250a | ||
|
|
59fcef39ff | ||
|
|
d0ff45500b | ||
|
|
0ace832166 | ||
|
|
19887f8742 | ||
|
|
7f97d166bf | ||
|
|
0de2266a72 | ||
|
|
8f06b35dfc | ||
|
|
a97e7bb22d | ||
|
|
fc47e9443b | ||
|
|
e144b0f0f9 | ||
|
|
a024c1b162 | ||
|
|
581e2f22d5 | ||
|
|
5232f2abdd | ||
|
|
cb52b80f7d | ||
|
|
d0ec9301ab | ||
|
|
9abd0fb92f | ||
|
|
43d77729c5 | ||
|
|
04b3c89cf5 | ||
|
|
09e2075c68 | ||
|
|
3bd9684ca5 | ||
|
|
414900fefb | ||
|
|
35484ca086 | ||
|
|
603765fe92 | ||
|
|
80140732c3 | ||
|
|
c00647ace0 | ||
|
|
2a2ee81957 | ||
|
|
b620c433c0 | ||
|
|
b80f00900d | ||
|
|
a32fc10f1b | ||
|
|
672ff96754 | ||
|
|
8132989f91 | ||
|
|
ca54bbfcc9 | ||
|
|
e19e9a1f2b | ||
|
|
e89e64263c | ||
|
|
f56bdd29ff | ||
|
|
9eff9fa703 | ||
|
|
c1f156fd2b | ||
|
|
4342d7aa17 | ||
|
|
8a2d7a3e11 | ||
|
|
af3ea5a321 | ||
|
|
de4f610540 | ||
|
|
770f8bd1c3 | ||
|
|
1ab942e0a2 | ||
|
|
c09b7b5d6d | ||
|
|
710454119f | ||
|
|
25e6d694e1 | ||
|
|
19a20b3b13 | ||
|
|
bd5b70c3cd | ||
|
|
ec5439e4d4 | ||
|
|
fd509e188a | ||
|
|
a5a839e72a | ||
|
|
3b53952dbe | ||
|
|
e502202de7 | ||
|
|
2479ce9123 | ||
|
|
d3772d4abd | ||
|
|
e9f36a7e45 | ||
|
|
f036bf9353 | ||
|
|
f4679cc870 | ||
|
|
7b116b0207 | ||
|
|
ffb19381f1 | ||
|
|
1525cbfb93 | ||
|
|
b83059c828 | ||
|
|
c7226ec28f | ||
|
|
6541e789fb | ||
|
|
c1b5772f0f | ||
|
|
6627c352e6 | ||
|
|
6de403e0ac | ||
|
|
75f902f57e | ||
|
|
8db4641455 | ||
|
|
89042439b8 | ||
|
|
f34ebf733d | ||
|
|
84271a2dac | ||
|
|
e753c51e34 | ||
|
|
65de739489 | ||
|
|
3f9d052218 | ||
|
|
4314dc251f | ||
|
|
e0de521388 | ||
|
|
3a282702d9 | ||
|
|
7759ae26fd | ||
|
|
4be91a103d | ||
|
|
a4b64dec39 | ||
|
|
3c0d02f057 | ||
|
|
29e973d060 | ||
|
|
12b2cfa9b5 | ||
|
|
c4810da82f | ||
|
|
0aa22d9d91 | ||
|
|
670bd0ce48 | ||
|
|
4803721120 | ||
|
|
755a2a8291 | ||
|
|
3bd31b91fb | ||
|
|
25e00556d0 | ||
|
|
8a90ad9e28 | ||
|
|
3f19be9717 | ||
|
|
13fe5857b3 | ||
|
|
5327d2dd1a | ||
|
|
da4048a9ec | ||
|
|
b4551cc127 | ||
|
|
3337107e79 | ||
|
|
f7609e9cb1 | ||
|
|
52671842d5 | ||
|
|
fc4a21e491 | ||
|
|
70c8970555 | ||
|
|
fa32411ab1 | ||
|
|
29c40622d3 | ||
|
|
80a15977ff | ||
|
|
e406c57ec9 | ||
|
|
9232fa06e4 | ||
|
|
94370eda54 | ||
|
|
52561d4f7c | ||
|
|
9381f187a4 | ||
|
|
445b0f6f94 | ||
|
|
19932bce53 | ||
|
|
cc5256b8fb | ||
|
|
236d5f8742 | ||
|
|
2df433eb0a | ||
|
|
44838937d1 | ||
|
|
8b6a5eef4c | ||
|
|
6fb55b363a | ||
|
|
7c8e7d6eb0 | ||
|
|
eb2338249f | ||
|
|
d499c18e63 | ||
|
|
c95c8a04ef | ||
|
|
312de6b3a3 | ||
|
|
9d839f1f53 | ||
|
|
475f6f5f82 | ||
|
|
fd9ceb7381 | ||
|
|
154b070eae | ||
|
|
8a3dcbf10f | ||
|
|
bf176c405a | ||
|
|
cf8e6d8d86 | ||
|
|
4a6a53c1ad | ||
|
|
896ba7e3fa | ||
|
|
fafc4a6042 | ||
|
|
a298b0790b | ||
|
|
1baf0da627 | ||
|
|
fc4cd39cdd | ||
|
|
2486c9af35 | ||
|
|
515d1bdbd3 | ||
|
|
ff7db218b1 | ||
|
|
350b8e09e6 | ||
|
|
1b91218a60 | ||
|
|
aa0fc339c0 | ||
|
|
bbf6e9ea47 | ||
|
|
84524e0712 | ||
|
|
e2ce1d05ae | ||
|
|
0d75cd484b | ||
|
|
499382a9a9 | ||
|
|
f1aef33dd6 | ||
|
|
6c0f4c35f6 | ||
|
|
08b0629eca | ||
|
|
3704a18da5 | ||
|
|
a8784f9adf | ||
|
|
e7c08921eb | ||
|
|
5e35beb41a | ||
|
|
e980ced0b7 | ||
|
|
fee922c4be | ||
|
|
d2d28fd419 | ||
|
|
67007aed40 | ||
|
|
df1c3dfb67 | ||
|
|
689484216d | ||
|
|
51c6029fe5 | ||
|
|
0eee544d17 | ||
|
|
21cca21124 | ||
|
|
f837451633 | ||
|
|
cce4a569e4 | ||
|
|
4feea9d7ec | ||
|
|
7aff588bf0 | ||
|
|
41a046a69d | ||
|
|
e548bd5312 | ||
|
|
88098283c7 | ||
|
|
6c6ed29329 | ||
|
|
c4f4e492e5 | ||
|
|
c286e2c434 |
49
.coveragerc
49
.coveragerc
@@ -52,7 +52,7 @@ omit =
|
||||
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
|
||||
homeassistant/components/doorbird.py
|
||||
homeassistant/components/*/doorbird.py
|
||||
|
||||
@@ -71,6 +71,9 @@ omit =
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/gc100.py
|
||||
homeassistant/components/*/gc100.py
|
||||
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
@@ -107,6 +110,9 @@ omit =
|
||||
homeassistant/components/lametric.py
|
||||
homeassistant/components/*/lametric.py
|
||||
|
||||
homeassistant/components/linode.py
|
||||
homeassistant/components/*/linode.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
@@ -149,6 +155,9 @@ omit =
|
||||
homeassistant/components/rachio.py
|
||||
homeassistant/components/*/rachio.py
|
||||
|
||||
homeassistant/components/raincloud.py
|
||||
homeassistant/components/*/raincloud.py
|
||||
|
||||
homeassistant/components/raspihats.py
|
||||
homeassistant/components/*/raspihats.py
|
||||
|
||||
@@ -167,6 +176,9 @@ omit =
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/skybell.py
|
||||
homeassistant/components/*/skybell.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
@@ -179,8 +191,14 @@ omit =
|
||||
homeassistant/components/tesla.py
|
||||
homeassistant/components/*/tesla.py
|
||||
|
||||
homeassistant/components/thethingsnetwork.py
|
||||
homeassistant/components/*/thethingsnetwork.py
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/toon.py
|
||||
homeassistant/components/*/toon.py
|
||||
|
||||
homeassistant/components/tradfri.py
|
||||
homeassistant/components/*/tradfri.py
|
||||
|
||||
@@ -211,7 +229,7 @@ omit =
|
||||
homeassistant/components/wemo.py
|
||||
homeassistant/components/*/wemo.py
|
||||
|
||||
homeassistant/components/wink.py
|
||||
homeassistant/components/wink/*
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/xiaomi_aqara.py
|
||||
@@ -260,7 +278,10 @@ omit =
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/ring.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/camera/yi.py
|
||||
homeassistant/components/climate/ephember.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/flexit.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
@@ -311,6 +332,7 @@ omit =
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/fan/xiaomi_miio.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/ifttt.py
|
||||
@@ -398,7 +420,9 @@ omit =
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/clickatell.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
@@ -420,6 +444,7 @@ omit =
|
||||
homeassistant/components/notify/pushover.py
|
||||
homeassistant/components/notify/pushsafer.py
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/rocketchat.py
|
||||
homeassistant/components/notify/sendgrid.py
|
||||
homeassistant/components/notify/simplepush.py
|
||||
homeassistant/components/notify/slack.py
|
||||
@@ -429,8 +454,10 @@ omit =
|
||||
homeassistant/components/notify/telstra.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/notify/yessssms.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/prometheus.py
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
@@ -447,7 +474,6 @@ omit =
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
@@ -455,6 +481,7 @@ omit =
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deluge.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
@@ -482,17 +509,18 @@ omit =
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hddtemp.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/hydroquebec.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/irish_rail_transport.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/luftdaten.py
|
||||
homeassistant/components/sensor/lyft.py
|
||||
homeassistant/components/sensor/metoffice.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
@@ -500,6 +528,7 @@ omit =
|
||||
homeassistant/components/sensor/mopar.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/mvglive.py
|
||||
homeassistant/components/sensor/nederlandse_spoorwegen.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
@@ -522,6 +551,7 @@ omit =
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/shodan.py
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
@@ -535,18 +565,22 @@ omit =
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/sytadin.py
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/tibber.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/travisci.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/whois.py
|
||||
homeassistant/components/sensor/worldtidesinfo.py
|
||||
homeassistant/components/sensor/worxlandroid.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
@@ -558,6 +592,7 @@ omit =
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/broadlink.py
|
||||
homeassistant/components/switch/deluge.py
|
||||
homeassistant/components/switch/digitalloggers.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
@@ -570,18 +605,20 @@ omit =
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pilight.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rainbird.py
|
||||
homeassistant/components/switch/rainmachine.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/snmp.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/switch/xiaomi_miio.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
homeassistant/components/tts/microsoft.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/vacuum/roomba.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "homeassistant/components/frontend/www_static/home-assistant-polymer"]
|
||||
path = homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
url = https://github.com/home-assistant/home-assistant-polymer.git
|
||||
|
||||
30
CODEOWNERS
30
CODEOWNERS
@@ -29,6 +29,9 @@ homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/websocket_api.py @home-assistant/core
|
||||
homeassistant/components/zone.py @home-assistant/core
|
||||
|
||||
# To monitor non-pypi additions
|
||||
requirements_all.txt @andrey-git
|
||||
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
@@ -36,6 +39,33 @@ homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
|
||||
# Indiviudal components
|
||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/switch/rainmachine.py @bachya
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/*/tradfri.py @ggravlingen
|
||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# This way, the development image and the production image are kept in sync.
|
||||
|
||||
FROM python:3.6
|
||||
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
|
||||
# Uncomment any of the following lines to disable the installation.
|
||||
#ENV INSTALL_TELLSTICK no
|
||||
@@ -11,7 +11,6 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
#ENV INSTALL_FFMPEG no
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_COAP_CLIENT no
|
||||
#ENV INSTALL_SSOCR no
|
||||
|
||||
VOLUME /config
|
||||
@@ -25,11 +24,10 @@ RUN virtualization/Docker/setup_docker_prereqs
|
||||
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
|
||||
# Uninstall enum34 because some depenndecies install it but breaks Python 3.4+.
|
||||
# Uninstall enum34 because some dependencies install it but breaks Python 3.4+.
|
||||
# See PR #8103 for more info.
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
include README.rst
|
||||
include LICENSE.md
|
||||
graft homeassistant
|
||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
BIN
docs/screenshot-components.png
Executable file → Normal file
BIN
docs/screenshot-components.png
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 205 KiB |
@@ -117,7 +117,11 @@ def linkcode_resolve(domain, info):
|
||||
linespec = "#L%d" % (lineno + 1)
|
||||
else:
|
||||
linespec = ""
|
||||
fn = relpath(fn, start='../')
|
||||
index = fn.find("/homeassistant/")
|
||||
if index == -1:
|
||||
index = 0
|
||||
|
||||
fn = fn[index:]
|
||||
|
||||
return '{}/blob/{}/{}{}'.format(GITHUB_URL, code_branch, fn, linespec)
|
||||
|
||||
|
||||
@@ -11,13 +11,11 @@ from typing import Any, Optional, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant import (
|
||||
core, config as conf_util, loader, components as core_components)
|
||||
from homeassistant.components import persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.package import async_get_user_site, get_user_site
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
@@ -83,6 +81,18 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
This method is a coroutine.
|
||||
"""
|
||||
start = time()
|
||||
|
||||
if enable_log:
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
|
||||
if sys.version_info[:2] < (3, 5):
|
||||
_LOGGER.warning(
|
||||
'Python 3.4 support has been deprecated and will be removed in '
|
||||
'the beginning of 2018. Please upgrade Python or your operating '
|
||||
'system. More info: https://home-assistant.io/blog/2017/10/06/'
|
||||
'deprecating-python-3.4-support/'
|
||||
)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
@@ -93,9 +103,6 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
|
||||
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
if enable_log:
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
_LOGGER.warning("Skipping pip installation of required modules. "
|
||||
|
||||
@@ -10,6 +10,7 @@ Component design guidelines:
|
||||
import asyncio
|
||||
import itertools as it
|
||||
import logging
|
||||
import os
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
@@ -110,6 +111,11 @@ def async_reload_core_config(hass):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up general services related to Home Assistant."""
|
||||
descriptions = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_turn_service(service):
|
||||
"""Handle calls to homeassistant.turn_on/off."""
|
||||
@@ -149,11 +155,14 @@ def async_setup(hass, config):
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TURN_OFF])
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TURN_ON])
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TOGGLE])
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_core_service(call):
|
||||
@@ -178,11 +187,14 @@ def async_setup(hass, config):
|
||||
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_STOP])
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_RESTART])
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_CHECK_CONFIG])
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_reload_config(call):
|
||||
@@ -197,6 +209,7 @@ def async_setup(hass, config):
|
||||
hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config,
|
||||
descriptions[ha.DOMAIN][SERVICE_RELOAD_CORE_CONFIG])
|
||||
|
||||
return True
|
||||
|
||||
@@ -10,24 +10,23 @@ from functools import partial
|
||||
from os import path
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME,
|
||||
ATTR_ENTITY_ID, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_EXCLUDE, CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.11.8']
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME,
|
||||
CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.12.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = "Data provided by goabode.com"
|
||||
CONF_LIGHTS = "lights"
|
||||
CONF_POLLING = "polling"
|
||||
CONF_POLLING = 'polling'
|
||||
|
||||
DOMAIN = 'abode'
|
||||
|
||||
@@ -93,10 +92,9 @@ class AbodeSystem(object):
|
||||
def __init__(self, username, password, name, polling, exclude, lights):
|
||||
"""Initialize the system."""
|
||||
import abodepy
|
||||
self.abode = abodepy.Abode(username, password,
|
||||
auto_login=True,
|
||||
get_devices=True,
|
||||
get_automations=True)
|
||||
self.abode = abodepy.Abode(
|
||||
username, password, auto_login=True, get_devices=True,
|
||||
get_automations=True)
|
||||
self.name = name
|
||||
self.polling = polling
|
||||
self.exclude = exclude
|
||||
@@ -210,7 +208,7 @@ def setup_hass_services(hass):
|
||||
|
||||
|
||||
def setup_hass_events(hass):
|
||||
"""Home assistant start and stop callbacks."""
|
||||
"""Home Assistant start and stop callbacks."""
|
||||
def startup(event):
|
||||
"""Listen for push events."""
|
||||
hass.data[DOMAIN].abode.events.start()
|
||||
|
||||
@@ -124,20 +124,13 @@ def async_setup(hass, config):
|
||||
|
||||
method = "async_{}".format(SERVICE_TO_METHOD[service.service])
|
||||
|
||||
update_tasks = []
|
||||
for alarm in target_alarms:
|
||||
yield from getattr(alarm, method)(code)
|
||||
|
||||
update_tasks = []
|
||||
for alarm in target_alarms:
|
||||
if not alarm.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.async_add_job(
|
||||
alarm.async_update_ha_state(True))
|
||||
if hasattr(alarm, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
update_tasks.append(alarm.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
121
homeassistant/components/alarm_control_panel/arlo.py
Normal file
121
homeassistant/components/alarm_control_panel/arlo.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Support for Arlo Alarm Control Panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.arlo import (DATA_ARLO, CONF_ATTRIBUTION)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ARMED = 'armed'
|
||||
|
||||
CONF_HOME_MODE_NAME = 'home_mode_name'
|
||||
|
||||
DEPENDENCIES = ['arlo']
|
||||
|
||||
DISARMED = 'disarmed'
|
||||
|
||||
ICON = 'mdi:security'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Arlo Alarm Control Panels."""
|
||||
data = hass.data[DATA_ARLO]
|
||||
|
||||
if not data.base_stations:
|
||||
return
|
||||
|
||||
home_mode_name = config.get(CONF_HOME_MODE_NAME)
|
||||
base_stations = []
|
||||
for base_station in data.base_stations:
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name))
|
||||
async_add_devices(base_stations, True)
|
||||
|
||||
|
||||
class ArloBaseStation(AlarmControlPanel):
|
||||
"""Representation of an Arlo Alarm Control Panel."""
|
||||
|
||||
def __init__(self, data, home_mode_name):
|
||||
"""Initialize the alarm control panel."""
|
||||
self._base_station = data
|
||||
self._home_mode_name = home_mode_name
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the device."""
|
||||
# PyArlo sometimes returns None for mode. So retry 3 times before
|
||||
# returning None.
|
||||
num_retries = 3
|
||||
i = 0
|
||||
while i < num_retries:
|
||||
mode = self._base_station.mode
|
||||
if mode:
|
||||
self._state = self._get_state_from_mode(mode)
|
||||
return
|
||||
i += 1
|
||||
self._state = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._base_station.mode = DISARMED
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._base_station.mode = ARMED
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command. Uses custom mode."""
|
||||
self._base_station.mode = self._home_mode_name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the base station."""
|
||||
return self._base_station.name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._base_station.device_id
|
||||
}
|
||||
|
||||
def _get_state_from_mode(self, mode):
|
||||
"""Convert Arlo mode to Home Assistant state."""
|
||||
if mode == ARMED:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif mode == DISARMED:
|
||||
return STATE_ALARM_DISARMED
|
||||
elif mode == self._home_mode_name:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
return None
|
||||
@@ -107,7 +107,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
newstate = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
if not newstate == self._state:
|
||||
_LOGGER.info("State Chnage from %s to %s", self._state, newstate)
|
||||
_LOGGER.info("State Change from %s to %s", self._state, newstate)
|
||||
self._state = newstate
|
||||
return self._state
|
||||
|
||||
|
||||
@@ -18,13 +18,14 @@ from homeassistant.const import (
|
||||
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.20']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.22']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_REPORT_SERVER_CODES = 'report_server_codes'
|
||||
CONF_REPORT_SERVER_ENABLED = 'report_server_enabled'
|
||||
CONF_REPORT_SERVER_PORT = 'report_server_port'
|
||||
CONF_REPORT_SERVER_CODES_IGNORE = 'ignore'
|
||||
|
||||
DEFAULT_NAME = 'Egardia'
|
||||
DEFAULT_PORT = 80
|
||||
@@ -148,9 +149,15 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def parsestatus(self, status):
|
||||
"""Parse the status."""
|
||||
newstatus = ([v for k, v in STATES.items()
|
||||
if status.upper() == k][0])
|
||||
self._status = newstatus
|
||||
_LOGGER.debug("Parsing status %s", status)
|
||||
# Ignore the statuscode if it is IGNORE
|
||||
if status.lower().strip() != CONF_REPORT_SERVER_CODES_IGNORE:
|
||||
_LOGGER.debug("Not ignoring status")
|
||||
newstatus = ([v for k, v in STATES.items()
|
||||
if status.upper() == k][0])
|
||||
self._status = newstatus
|
||||
else:
|
||||
_LOGGER.error("Ignoring status")
|
||||
|
||||
def update(self):
|
||||
"""Update the alarm status."""
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.manual_mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
@@ -13,9 +14,9 @@ import voluptuous as vol
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM,
|
||||
CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
@@ -28,6 +29,7 @@ from homeassistant.helpers.event import track_point_in_time
|
||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
||||
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
@@ -35,11 +37,32 @@ DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
||||
DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
|
||||
DEFAULT_DISARM = 'DISARM'
|
||||
|
||||
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED]
|
||||
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
STATE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PLATFORM): 'manual_mqtt',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
@@ -49,12 +72,17 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||
})
|
||||
}), _state_validator))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -73,7 +101,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
config.get(mqtt.CONF_QOS),
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY))])
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||
config.get(CONF_PAYLOAD_ARM_NIGHT),
|
||||
config)])
|
||||
|
||||
|
||||
class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
@@ -89,7 +119,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
def __init__(self, hass, name, code, pending_time,
|
||||
trigger_time, disarm_after_trigger,
|
||||
state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away):
|
||||
payload_disarm, payload_arm_home, payload_arm_away,
|
||||
payload_arm_night, config):
|
||||
"""Init the manual MQTT alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
@@ -101,12 +132,18 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
self._pre_trigger_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._pending_time_by_state = {}
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
self._pending_time_by_state[state] = datetime.timedelta(
|
||||
seconds=config[state][CONF_PENDING_TIME])
|
||||
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._payload_arm_night = payload_arm_night
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -121,23 +158,27 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
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
|
||||
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state_ts + self._pending_time > dt_util.utcnow():
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time +
|
||||
elif (self._state_ts + self._pending_time_by_state[self._state] +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
return self._pre_trigger_state
|
||||
else:
|
||||
self._state = self._pre_trigger_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
return self._state
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
return self._state_ts + pending_time > dt_util.utcnow()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters."""
|
||||
@@ -157,44 +198,47 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
self._update_state(STATE_ALARM_ARMED_HOME)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
self._update_state(STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_NIGHT):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._trigger_time:
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
|
||||
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
self._state_ts + pending_time)
|
||||
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time + self._trigger_time)
|
||||
self._state_ts + self._trigger_time + pending_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
@@ -203,6 +247,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
|
||||
@@ -221,6 +275,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
self.async_alarm_arm_home(self._code)
|
||||
elif payload == self._payload_arm_away:
|
||||
self.async_alarm_arm_away(self._code)
|
||||
elif payload == self._payload_arm_night:
|
||||
self.async_alarm_arm_night(self._code)
|
||||
else:
|
||||
_LOGGER.warning("Received unexpected payload: %s", payload)
|
||||
return
|
||||
|
||||
@@ -1,65 +1,61 @@
|
||||
alarm_disarm:
|
||||
description: Send the alarm the command for disarm
|
||||
# Describes the format for available alarm control panel services
|
||||
|
||||
alarm_disarm:
|
||||
description: Send the alarm the command for disarm.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to disarm
|
||||
description: Name of alarm control panel to disarm.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to disarm the alarm control panel with
|
||||
description: An optional code to disarm the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_arm_home:
|
||||
description: Send the alarm the command for arm home
|
||||
|
||||
description: Send the alarm the command for arm home.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm home
|
||||
description: Name of alarm control panel to arm home.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm home the alarm control panel with
|
||||
description: An optional code to arm home the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_arm_away:
|
||||
description: Send the alarm the command for arm away
|
||||
|
||||
description: Send the alarm the command for arm away.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm away
|
||||
description: Name of alarm control panel to arm away.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm away the alarm control panel with
|
||||
description: An optional code to arm away the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_arm_night:
|
||||
description: Send the alarm the command for arm night
|
||||
|
||||
description: Send the alarm the command for arm night.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm night
|
||||
description: Name of alarm control panel to arm night.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm night the alarm control panel with
|
||||
description: An optional code to arm night the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_trigger:
|
||||
description: Send the alarm the command for trigger
|
||||
|
||||
description: Send the alarm the command for trigger.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to trigger
|
||||
description: Name of alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with
|
||||
description: An optional code to trigger the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
envisalink_alarm_keypress:
|
||||
description: Send custom keypresses to the alarm
|
||||
|
||||
description: Send custom keypresses to the alarm.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel (1-6 characters)'
|
||||
description: 'String to send to the alarm panel (1-6 characters).'
|
||||
example: '*71'
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.11']
|
||||
REQUIREMENTS = ['total_connect_client==0.12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,93 +1,110 @@
|
||||
"""Support for alexa Smart Home Skill API."""
|
||||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
from uuid import uuid4
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
from homeassistant.components import switch, light
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_HEADER = 'header'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_NAMESPACE = 'namespace'
|
||||
ATTR_MESSAGE_ID = 'messageId'
|
||||
ATTR_PAYLOAD = 'payload'
|
||||
ATTR_PAYLOAD_VERSION = 'payloadVersion'
|
||||
API_DIRECTIVE = 'directive'
|
||||
API_EVENT = 'event'
|
||||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
API_ENDPOINT = 'endpoint'
|
||||
|
||||
|
||||
MAPPING_COMPONENT = {
|
||||
switch.DOMAIN: ['SWITCH', ('turnOff', 'turnOn'), None],
|
||||
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
|
||||
light.DOMAIN: [
|
||||
'LIGHT', ('turnOff', 'turnOn'), {
|
||||
light.SUPPORT_BRIGHTNESS: 'setPercentage'
|
||||
'LIGHT', ('Alexa.PowerController',), {
|
||||
light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController',
|
||||
light.SUPPORT_RGB_COLOR: 'Alexa.ColorController',
|
||||
light.SUPPORT_XY_COLOR: 'Alexa.ColorController',
|
||||
light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController',
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def mapping_api_function(name):
|
||||
"""Return function pointer to api function for name.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
mapping = {
|
||||
'DiscoverAppliancesRequest': async_api_discovery,
|
||||
'TurnOnRequest': async_api_turn_on,
|
||||
'TurnOffRequest': async_api_turn_off,
|
||||
'SetPercentageRequest': async_api_set_percentage,
|
||||
}
|
||||
return mapping.get(name, None)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, message):
|
||||
"""Handle incomming API messages."""
|
||||
assert int(message[ATTR_HEADER][ATTR_PAYLOAD_VERSION]) == 2
|
||||
"""Handle incoming API messages."""
|
||||
assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
|
||||
|
||||
# Read head data
|
||||
message = message[API_DIRECTIVE]
|
||||
namespace = message[API_HEADER]['namespace']
|
||||
name = message[API_HEADER]['name']
|
||||
|
||||
# Do we support this API request?
|
||||
funct_ref = mapping_api_function(message[ATTR_HEADER][ATTR_NAME])
|
||||
funct_ref = HANDLERS.get((namespace, name))
|
||||
if not funct_ref:
|
||||
_LOGGER.warning(
|
||||
"Unsupported API request %s", message[ATTR_HEADER][ATTR_NAME])
|
||||
"Unsupported API request %s/%s", namespace, name)
|
||||
return api_error(message)
|
||||
|
||||
return (yield from funct_ref(hass, message))
|
||||
|
||||
|
||||
def api_message(name, namespace, payload=None):
|
||||
"""Create a API formated response message.
|
||||
def api_message(request, name='Response', namespace='Alexa', payload=None):
|
||||
"""Create a API formatted response message.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
payload = payload or {}
|
||||
return {
|
||||
ATTR_HEADER: {
|
||||
ATTR_MESSAGE_ID: uuid4(),
|
||||
ATTR_NAME: name,
|
||||
ATTR_NAMESPACE: namespace,
|
||||
ATTR_PAYLOAD_VERSION: '2',
|
||||
},
|
||||
ATTR_PAYLOAD: payload,
|
||||
|
||||
response = {
|
||||
API_EVENT: {
|
||||
API_HEADER: {
|
||||
'namespace': namespace,
|
||||
'name': name,
|
||||
'messageId': str(uuid4()),
|
||||
'payloadVersion': '3',
|
||||
},
|
||||
API_PAYLOAD: payload,
|
||||
}
|
||||
}
|
||||
|
||||
# If a correlation token exsits, add it to header / Need by Async requests
|
||||
token = request[API_HEADER].get('correlationToken')
|
||||
if token:
|
||||
response[API_EVENT][API_HEADER]['correlationToken'] = token
|
||||
|
||||
def api_error(request, exc='DriverInternalError'):
|
||||
"""Create a API formated error response.
|
||||
# Extend event with endpoint object / Need by Async requests
|
||||
if API_ENDPOINT in request:
|
||||
response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
|
||||
"""Create a API formatted error response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return api_message(exc, request[ATTR_HEADER][ATTR_NAMESPACE])
|
||||
payload = {
|
||||
'type': error_type,
|
||||
'message': error_message,
|
||||
}
|
||||
|
||||
return api_message(request, name='ErrorResponse', payload=payload)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||
@asyncio.coroutine
|
||||
def async_api_discovery(hass, request):
|
||||
"""Create a API formated discovery response.
|
||||
"""Create a API formatted discovery response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
discovered_appliances = []
|
||||
discovery_endpoints = []
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
class_data = MAPPING_COMPONENT.get(entity.domain)
|
||||
@@ -95,35 +112,42 @@ def async_api_discovery(hass, request):
|
||||
if not class_data:
|
||||
continue
|
||||
|
||||
appliance = {
|
||||
'actions': [],
|
||||
'applianceTypes': [class_data[0]],
|
||||
endpoint = {
|
||||
'displayCategories': [class_data[0]],
|
||||
'additionalApplianceDetails': {},
|
||||
'applianceId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyDescription': '',
|
||||
'endpointId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyName': entity.name,
|
||||
'isReachable': True,
|
||||
'description': '',
|
||||
'manufacturerName': 'Unknown',
|
||||
'modelName': 'Unknown',
|
||||
'version': 'Unknown',
|
||||
}
|
||||
actions = set()
|
||||
|
||||
# static actions
|
||||
if class_data[1]:
|
||||
appliance['actions'].extend(list(class_data[1]))
|
||||
actions |= set(class_data[1])
|
||||
|
||||
# dynamic actions
|
||||
if class_data[2]:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
for feature, action_name in class_data[2].items():
|
||||
if feature & supported > 0:
|
||||
appliance['actions'].append(action_name)
|
||||
actions.add(action_name)
|
||||
|
||||
discovered_appliances.append(appliance)
|
||||
# Write action into capabilities
|
||||
capabilities = []
|
||||
for action in actions:
|
||||
capabilities.append({
|
||||
'type': 'AlexaInterface',
|
||||
'interface': action,
|
||||
'version': 3,
|
||||
})
|
||||
|
||||
endpoint['capabilities'] = capabilities
|
||||
discovery_endpoints.append(endpoint)
|
||||
|
||||
return api_message(
|
||||
'DiscoverAppliancesResponse', 'Alexa.ConnectedHome.Discovery',
|
||||
payload={'discoveredAppliances': discovered_appliances})
|
||||
request, name='Discover.Response', namespace='Alexa.Discovery',
|
||||
payload={'endpoints': discovery_endpoints})
|
||||
|
||||
|
||||
def extract_entity(funct):
|
||||
@@ -131,21 +155,21 @@ def extract_entity(funct):
|
||||
@asyncio.coroutine
|
||||
def async_api_entity_wrapper(hass, request):
|
||||
"""Process a turn on request."""
|
||||
entity_id = \
|
||||
request[ATTR_PAYLOAD]['appliance']['applianceId'].replace('#', '.')
|
||||
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
|
||||
|
||||
# extract state object
|
||||
entity = hass.states.get(entity_id)
|
||||
if not entity:
|
||||
_LOGGER.error("Can't process %s for %s",
|
||||
request[ATTR_HEADER][ATTR_NAME], entity_id)
|
||||
return api_error(request)
|
||||
request[API_HEADER]['name'], entity_id)
|
||||
return api_error(request, error_type='NO_SUCH_ENDPOINT')
|
||||
|
||||
return (yield from funct(hass, request, entity))
|
||||
|
||||
return async_api_entity_wrapper
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_on(hass, request, entity):
|
||||
@@ -154,9 +178,10 @@ def async_api_turn_on(hass, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message('TurnOnConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_off(hass, request, entity):
|
||||
@@ -165,21 +190,122 @@ def async_api_turn_off(hass, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message('TurnOffConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_percentage(hass, request, entity):
|
||||
"""Process a set percentage request."""
|
||||
if entity.domain == light.DOMAIN:
|
||||
brightness = request[ATTR_PAYLOAD]['percentageState']['value']
|
||||
def async_api_set_brightness(hass, request, entity):
|
||||
"""Process a set brightness request."""
|
||||
brightness = int(request[API_PAYLOAD]['brightness'])
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_brightness(hass, request, entity):
|
||||
"""Process a adjust brightness request."""
|
||||
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
|
||||
|
||||
# read current state
|
||||
try:
|
||||
current = math.floor(
|
||||
int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100)
|
||||
except ZeroDivisionError:
|
||||
current = 0
|
||||
|
||||
# set brightness
|
||||
brightness = max(0, brightness_delta + current)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_color(hass, request, entity):
|
||||
"""Process a set color request."""
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
rgb = color_util.color_hsb_to_RGB(
|
||||
float(request[API_PAYLOAD]['color']['hue']),
|
||||
float(request[API_PAYLOAD]['color']['saturation']),
|
||||
float(request[API_PAYLOAD]['color']['brightness'])
|
||||
)
|
||||
|
||||
if supported & light.SUPPORT_RGB_COLOR > 0:
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS: brightness,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=True)
|
||||
else:
|
||||
return api_error(request)
|
||||
xyz = color_util.color_RGB_to_xy(*rgb)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
|
||||
light.ATTR_BRIGHTNESS: xyz[2],
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(
|
||||
'SetPercentageConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_color_temperature(hass, request, entity):
|
||||
"""Process a set color temperature request."""
|
||||
kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_KELVIN: kelvin,
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(
|
||||
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_decrease_color_temp(hass, request, entity):
|
||||
"""Process a decrease color temperature request."""
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
|
||||
|
||||
value = min(max_mireds, current + 50)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(
|
||||
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_increase_color_temp(hass, request, entity):
|
||||
"""Process a increase color temperature request."""
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
||||
|
||||
value = max(min_mireds, current - 50)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -262,7 +262,11 @@ class APIEventView(HomeAssistantView):
|
||||
def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
body = yield from request.text()
|
||||
event_data = json.loads(body) if body else None
|
||||
try:
|
||||
event_data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message('Event data should be valid JSON',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
return self.json_message('Event data should be a JSON object',
|
||||
@@ -309,7 +313,11 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
"""
|
||||
hass = request.app['hass']
|
||||
body = yield from request.text()
|
||||
data = json.loads(body) if body else None
|
||||
try:
|
||||
data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message('Data should be valid JSON',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
yield from hass.services.async_call(domain, service, data, True)
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.components.discovery import SERVICE_APPLE_TV
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.4']
|
||||
REQUIREMENTS = ['pyatv==0.3.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
This component provides basic support for Netgear Arlo IP cameras.
|
||||
This component provides support for Netgear Arlo IP cameras.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/arlo/
|
||||
@@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.0.4']
|
||||
REQUIREMENTS = ['pyarlo==0.0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,7 +23,7 @@ DEFAULT_BRAND = 'Netgear Arlo'
|
||||
DOMAIN = 'arlo'
|
||||
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Camera Setup'
|
||||
NOTIFICATION_TITLE = 'Arlo Component Setup'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
||||
@@ -29,18 +29,27 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data = config.get(CONF_EVENT_DATA)
|
||||
event_data_schema = vol.Schema(
|
||||
config.get(CONF_EVENT_DATA),
|
||||
extra=vol.ALLOW_EXTRA) if config.get(CONF_EVENT_DATA) else None
|
||||
|
||||
@callback
|
||||
def handle_event(event):
|
||||
"""Listen for events and calls the action when data matches."""
|
||||
if not event_data or all(val == event.data.get(key) for key, val
|
||||
in event_data.items()):
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
if event_data_schema:
|
||||
# Check that the event data matches the configured
|
||||
# schema if one was provided
|
||||
try:
|
||||
event_data_schema(event.data)
|
||||
except vol.Invalid:
|
||||
# If event data doesn't match requested schema, skip event
|
||||
return
|
||||
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
|
||||
return hass.bus.async_listen(event_type, handle_event)
|
||||
|
||||
@@ -38,13 +38,14 @@ def async_trigger(hass, config, action):
|
||||
time_delta = config.get(CONF_FOR)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
async_remove_track_same = None
|
||||
already_triggered = False
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
@callback
|
||||
def check_numeric_state(entity, from_s, to_s):
|
||||
"""Return True if they should trigger."""
|
||||
"""Return True if criteria are now met."""
|
||||
if to_s is None:
|
||||
return False
|
||||
|
||||
@@ -56,51 +57,39 @@ def async_trigger(hass, config, action):
|
||||
'above': above,
|
||||
}
|
||||
}
|
||||
|
||||
# If new one doesn't match, nothing to do
|
||||
if not condition.async_numeric_state(
|
||||
hass, to_s, below, above, value_template, variables):
|
||||
return False
|
||||
|
||||
return True
|
||||
return condition.async_numeric_state(
|
||||
hass, to_s, below, above, value_template, variables)
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal async_remove_track_same
|
||||
|
||||
if not check_numeric_state(entity, from_s, to_s):
|
||||
return
|
||||
|
||||
variables = {
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity,
|
||||
'below': below,
|
||||
'above': above,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
}
|
||||
}
|
||||
|
||||
# Only match if old didn't exist or existed but didn't match
|
||||
# Written as: skip if old one did exist and matched
|
||||
if from_s is not None and condition.async_numeric_state(
|
||||
hass, from_s, below, above, value_template, variables):
|
||||
return
|
||||
nonlocal already_triggered, async_remove_track_same
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, variables)
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity,
|
||||
'below': below,
|
||||
'above': above,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
}
|
||||
})
|
||||
|
||||
if not time_delta:
|
||||
call_action()
|
||||
return
|
||||
matching = check_numeric_state(entity, from_s, to_s)
|
||||
|
||||
async_remove_track_same = async_track_same_state(
|
||||
hass, True, time_delta, call_action, entity_ids=entity_id,
|
||||
async_check_func=check_numeric_state)
|
||||
if matching and not already_triggered:
|
||||
if time_delta:
|
||||
async_remove_track_same = async_track_same_state(
|
||||
hass, time_delta, call_action, entity_ids=entity_id,
|
||||
async_check_same_func=check_numeric_state)
|
||||
else:
|
||||
call_action()
|
||||
|
||||
already_triggered = matching
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Describes the format for available automation services
|
||||
|
||||
turn_on:
|
||||
description: Enable an automation.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the automation to turn on.
|
||||
@@ -8,7 +9,6 @@ turn_on:
|
||||
|
||||
turn_off:
|
||||
description: Disable an automation.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the automation to turn off.
|
||||
@@ -16,7 +16,6 @@ turn_off:
|
||||
|
||||
toggle:
|
||||
description: Toggle an automation.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the automation to toggle on/off.
|
||||
@@ -24,7 +23,6 @@ toggle:
|
||||
|
||||
trigger:
|
||||
description: Trigger the action of an automation.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the automation to trigger.
|
||||
|
||||
@@ -65,7 +65,9 @@ def async_trigger(hass, config, action):
|
||||
return
|
||||
|
||||
async_remove_track_same = async_track_same_state(
|
||||
hass, to_s.state, time_delta, call_action, entity_ids=entity_id)
|
||||
hass, time_delta, call_action,
|
||||
lambda _, _2, to_state: to_state.state == to_s.state,
|
||||
entity_ids=entity_id)
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
||||
@@ -11,19 +11,20 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
CONF_HOST, CONF_INCLUDE, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME,
|
||||
CONF_USERNAME, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_TRIGGER_TIME, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
|
||||
REQUIREMENTS = ['axis==12']
|
||||
REQUIREMENTS = ['axis==14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -87,10 +88,13 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
configurator.notify_errors(request_id,
|
||||
"Functionality mandatory.")
|
||||
return False
|
||||
|
||||
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
|
||||
callback_data[CONF_HOST] = host
|
||||
|
||||
if CONF_NAME not in callback_data:
|
||||
callback_data[CONF_NAME] = name
|
||||
|
||||
try:
|
||||
device_config = DEVICE_SCHEMA(callback_data)
|
||||
except vol.Invalid:
|
||||
@@ -101,7 +105,6 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
if setup_device(hass, config, device_config):
|
||||
config_file = _read_config(hass)
|
||||
config_file[serialnumber] = dict(device_config)
|
||||
del config_file[serialnumber]['hass']
|
||||
_write_config(hass, config_file)
|
||||
configurator.request_done(request_id)
|
||||
else:
|
||||
@@ -146,10 +149,10 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
def setup(hass, config):
|
||||
"""Common setup for Axis devices."""
|
||||
def _shutdown(call): # pylint: disable=unused-argument
|
||||
"""Stop the metadatastream on shutdown."""
|
||||
"""Stop the event stream on shutdown."""
|
||||
for serialnumber, device in AXIS_DEVICES.items():
|
||||
_LOGGER.info("Stopping metadatastream for %s.", serialnumber)
|
||||
device.stop_metadatastream()
|
||||
_LOGGER.info("Stopping event stream for %s.", serialnumber)
|
||||
device.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
|
||||
|
||||
@@ -162,7 +165,7 @@ def setup(hass, config):
|
||||
if serialnumber not in AXIS_DEVICES:
|
||||
config_file = _read_config(hass)
|
||||
if serialnumber in config_file:
|
||||
# Device config saved to file
|
||||
# Device config previously saved to file
|
||||
try:
|
||||
device_config = DEVICE_SCHEMA(config_file[serialnumber])
|
||||
device_config[CONF_HOST] = host
|
||||
@@ -178,10 +181,8 @@ def setup(hass, config):
|
||||
else:
|
||||
# Device already registered, but on a different IP
|
||||
device = AXIS_DEVICES[serialnumber]
|
||||
device.url = host
|
||||
async_dispatcher_send(hass,
|
||||
DOMAIN + '_' + device.name + '_new_ip',
|
||||
host)
|
||||
device.config.host = host
|
||||
dispatcher_send(hass, DOMAIN + '_' + device.name + '_new_ip', host)
|
||||
|
||||
# Register discovery service
|
||||
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
|
||||
@@ -202,10 +203,11 @@ def setup(hass, config):
|
||||
"""Service to send a message."""
|
||||
for _, device in AXIS_DEVICES.items():
|
||||
if device.name == call.data[CONF_NAME]:
|
||||
response = device.do_request(call.data[SERVICE_CGI],
|
||||
call.data[SERVICE_ACTION],
|
||||
call.data[SERVICE_PARAM])
|
||||
hass.bus.async_fire(SERVICE_VAPIX_CALL_RESPONSE, response)
|
||||
response = device.vapix.do_request(
|
||||
call.data[SERVICE_CGI],
|
||||
call.data[SERVICE_ACTION],
|
||||
call.data[SERVICE_PARAM])
|
||||
hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response)
|
||||
return True
|
||||
_LOGGER.info("Couldn\'t find device %s", call.data[CONF_NAME])
|
||||
return False
|
||||
@@ -216,7 +218,6 @@ def setup(hass, config):
|
||||
vapix_service,
|
||||
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -224,9 +225,28 @@ def setup_device(hass, config, device_config):
|
||||
"""Set up device."""
|
||||
from axis import AxisDevice
|
||||
|
||||
device_config['hass'] = hass
|
||||
device = AxisDevice(device_config) # Initialize device
|
||||
enable_metadatastream = False
|
||||
def signal_callback(action, event):
|
||||
"""Callback to configure events when initialized on event stream."""
|
||||
if action == 'add':
|
||||
event_config = {
|
||||
CONF_EVENT: event,
|
||||
CONF_NAME: device_config[CONF_NAME],
|
||||
ATTR_LOCATION: device_config[ATTR_LOCATION],
|
||||
CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME]
|
||||
}
|
||||
component = event.event_platform
|
||||
discovery.load_platform(hass,
|
||||
component,
|
||||
DOMAIN,
|
||||
event_config,
|
||||
config)
|
||||
|
||||
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE],
|
||||
EVENT_TYPES))
|
||||
device_config['events'] = event_types
|
||||
device_config['signal'] = signal_callback
|
||||
device = AxisDevice(hass.loop, **device_config)
|
||||
device.name = device_config[CONF_NAME]
|
||||
|
||||
if device.serial_number is None:
|
||||
# If there is no serial number a connection could not be made
|
||||
@@ -234,16 +254,10 @@ def setup_device(hass, config, device_config):
|
||||
return False
|
||||
|
||||
for component in device_config[CONF_INCLUDE]:
|
||||
if component in EVENT_TYPES:
|
||||
# Sensors are created by device calling event_initialized
|
||||
# when receiving initialize messages on metadatastream
|
||||
device.add_event_topic(convert(component, 'type', 'subscribe'))
|
||||
if not enable_metadatastream:
|
||||
enable_metadatastream = True
|
||||
else:
|
||||
if component == 'camera':
|
||||
camera_config = {
|
||||
CONF_HOST: device_config[CONF_HOST],
|
||||
CONF_NAME: device_config[CONF_NAME],
|
||||
CONF_HOST: device_config[CONF_HOST],
|
||||
CONF_PORT: device_config[CONF_PORT],
|
||||
CONF_USERNAME: device_config[CONF_USERNAME],
|
||||
CONF_PASSWORD: device_config[CONF_PASSWORD]
|
||||
@@ -254,17 +268,8 @@ def setup_device(hass, config, device_config):
|
||||
camera_config,
|
||||
config)
|
||||
|
||||
if enable_metadatastream:
|
||||
device.initialize_new_event = event_initialized
|
||||
if not device.initiate_metadatastream():
|
||||
hass.components.persistent_notification.create(
|
||||
'Dependency missing for sensors, '
|
||||
'please check documentation',
|
||||
title=DOMAIN,
|
||||
notification_id='axis_notification')
|
||||
|
||||
AXIS_DEVICES[device.serial_number] = device
|
||||
|
||||
hass.add_job(device.start)
|
||||
return True
|
||||
|
||||
|
||||
@@ -287,25 +292,16 @@ def _write_config(hass, config):
|
||||
outfile.write(data)
|
||||
|
||||
|
||||
def event_initialized(event):
|
||||
"""Register event initialized on metadatastream here."""
|
||||
hass = event.device_config('hass')
|
||||
discovery.load_platform(hass,
|
||||
convert(event.topic, 'topic', 'platform'),
|
||||
DOMAIN, {'axis_event': event})
|
||||
|
||||
|
||||
class AxisDeviceEvent(Entity):
|
||||
"""Representation of a Axis device event."""
|
||||
|
||||
def __init__(self, axis_event):
|
||||
def __init__(self, event_config):
|
||||
"""Initialize the event."""
|
||||
self.axis_event = axis_event
|
||||
self._event_class = convert(self.axis_event.topic, 'topic', 'class')
|
||||
self._name = '{}_{}_{}'.format(self.axis_event.device_name,
|
||||
convert(self.axis_event.topic,
|
||||
'topic', 'type'),
|
||||
self.axis_event = event_config[CONF_EVENT]
|
||||
self._name = '{}_{}_{}'.format(event_config[CONF_NAME],
|
||||
self.axis_event.event_type,
|
||||
self.axis_event.id)
|
||||
self.location = event_config[ATTR_LOCATION]
|
||||
self.axis_event.callback = self._update_callback
|
||||
|
||||
def _update_callback(self):
|
||||
@@ -321,7 +317,7 @@ class AxisDeviceEvent(Entity):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the event."""
|
||||
return self._event_class
|
||||
return self.axis_event.event_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -336,52 +332,6 @@ class AxisDeviceEvent(Entity):
|
||||
tripped = self.axis_event.is_tripped
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
||||
|
||||
location = self.axis_event.device_config(ATTR_LOCATION)
|
||||
if location:
|
||||
attr[ATTR_LOCATION] = location
|
||||
attr[ATTR_LOCATION] = self.location
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
def convert(item, from_key, to_key):
|
||||
"""Translate between Axis and HASS syntax."""
|
||||
for entry in REMAP:
|
||||
if entry[from_key] == item:
|
||||
return entry[to_key]
|
||||
|
||||
|
||||
REMAP = [{'type': 'motion',
|
||||
'class': 'motion',
|
||||
'topic': 'tns1:VideoAnalytics/tnsaxis:MotionDetection',
|
||||
'subscribe': 'onvif:VideoAnalytics/axis:MotionDetection',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'vmd3',
|
||||
'class': 'motion',
|
||||
'topic': 'tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1',
|
||||
'subscribe': 'onvif:RuleEngine/axis:VMD3/vmd3_video_1',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'pir',
|
||||
'class': 'motion',
|
||||
'topic': 'tns1:Device/tnsaxis:Sensor/PIR',
|
||||
'subscribe': 'onvif:Device/axis:Sensor/axis:PIR',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'sound',
|
||||
'class': 'sound',
|
||||
'topic': 'tns1:AudioSource/tnsaxis:TriggerLevel',
|
||||
'subscribe': 'onvif:AudioSource/axis:TriggerLevel',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'daynight',
|
||||
'class': 'light',
|
||||
'topic': 'tns1:VideoSource/tnsaxis:DayNightVision',
|
||||
'subscribe': 'onvif:VideoSource/axis:DayNightVision',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'tampering',
|
||||
'class': 'safety',
|
||||
'topic': 'tns1:VideoSource/tnsaxis:Tampering',
|
||||
'subscribe': 'onvif:VideoSource/axis:Tampering',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'input',
|
||||
'class': 'input',
|
||||
'topic': 'tns1:Device/tnsaxis:IO/Port',
|
||||
'subscribe': 'onvif:Device/axis:IO/Port',
|
||||
'platform': 'binary_sensor'}, ]
|
||||
|
||||
@@ -21,19 +21,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Axis device event."""
|
||||
add_devices([AxisBinarySensor(discovery_info['axis_event'], hass)], True)
|
||||
add_devices([AxisBinarySensor(hass, discovery_info)], True)
|
||||
|
||||
|
||||
class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
|
||||
"""Representation of a binary Axis event."""
|
||||
|
||||
def __init__(self, axis_event, hass):
|
||||
def __init__(self, hass, event_config):
|
||||
"""Initialize the binary sensor."""
|
||||
self.hass = hass
|
||||
self._state = False
|
||||
self._delay = axis_event.device_config(CONF_TRIGGER_TIME)
|
||||
self._delay = event_config[CONF_TRIGGER_TIME]
|
||||
self._timer = None
|
||||
AxisDeviceEvent.__init__(self, axis_event)
|
||||
AxisDeviceEvent.__init__(self, event_config)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -22,6 +22,10 @@ from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_OBSERVATIONS = 'observations'
|
||||
ATTR_PROBABILITY = 'probability'
|
||||
ATTR_PROBABILITY_THRESHOLD = 'probability_threshold'
|
||||
|
||||
CONF_OBSERVATIONS = 'observations'
|
||||
CONF_PRIOR = 'prior'
|
||||
CONF_PROBABILITY_THRESHOLD = 'probability_threshold'
|
||||
@@ -29,7 +33,8 @@ CONF_P_GIVEN_F = 'prob_given_false'
|
||||
CONF_P_GIVEN_T = 'prob_given_true'
|
||||
CONF_TO_STATE = 'to_state'
|
||||
|
||||
DEFAULT_NAME = 'BayesianBinary'
|
||||
DEFAULT_NAME = "Bayesian Binary Sensor"
|
||||
DEFAULT_PROBABILITY_THRESHOLD = 0.5
|
||||
|
||||
NUMERIC_STATE_SCHEMA = vol.Schema({
|
||||
CONF_PLATFORM: 'numeric_state',
|
||||
@@ -49,16 +54,14 @@ STATE_SCHEMA = vol.Schema({
|
||||
}, required=True)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME):
|
||||
cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
||||
vol.Required(CONF_OBSERVATIONS): vol.Schema(
|
||||
vol.All(cv.ensure_list, [vol.Any(NUMERIC_STATE_SCHEMA,
|
||||
STATE_SCHEMA)])
|
||||
),
|
||||
vol.Required(CONF_OBSERVATIONS):
|
||||
vol.Schema(vol.All(cv.ensure_list,
|
||||
[vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA)])),
|
||||
vol.Required(CONF_PRIOR): vol.Coerce(float),
|
||||
vol.Optional(CONF_PROBABILITY_THRESHOLD):
|
||||
vol.Coerce(float),
|
||||
vol.Optional(CONF_PROBABILITY_THRESHOLD,
|
||||
default=DEFAULT_PROBABILITY_THRESHOLD): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
@@ -73,16 +76,16 @@ def update_probability(prior, prob_true, prob_false):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Threshold sensor."""
|
||||
"""Set up the Bayesian Binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
observations = config.get(CONF_OBSERVATIONS)
|
||||
prior = config.get(CONF_PRIOR)
|
||||
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD, 0.5)
|
||||
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_devices([
|
||||
BayesianBinarySensor(name, prior, observations, probability_threshold,
|
||||
device_class)
|
||||
BayesianBinarySensor(
|
||||
name, prior, observations, probability_threshold, device_class)
|
||||
], True)
|
||||
|
||||
|
||||
@@ -107,7 +110,7 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
self.entity_obs = dict.fromkeys(to_observe, [])
|
||||
|
||||
for ind, obs in enumerate(self._observations):
|
||||
obs["id"] = ind
|
||||
obs['id'] = ind
|
||||
self.entity_obs[obs['entity_id']].append(obs)
|
||||
|
||||
self.watchers = {
|
||||
@@ -117,7 +120,7 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to hass."""
|
||||
"""Call when entity about to be added."""
|
||||
@callback
|
||||
# pylint: disable=invalid-name
|
||||
def async_threshold_sensor_state_listener(entity, old_state,
|
||||
@@ -135,8 +138,8 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
|
||||
prior = self.prior
|
||||
for obs in self.current_obs.values():
|
||||
prior = update_probability(prior, obs['prob_true'],
|
||||
obs['prob_false'])
|
||||
prior = update_probability(
|
||||
prior, obs['prob_true'], obs['prob_false'])
|
||||
self.probability = prior
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state, True)
|
||||
@@ -206,9 +209,9 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
'observations': [val for val in self.current_obs.values()],
|
||||
'probability': round(self.probability, 2),
|
||||
'probability_threshold': self._probability_threshold
|
||||
ATTR_OBSERVATIONS: [val for val in self.current_obs.values()],
|
||||
ATTR_PROBABILITY: round(self.probability, 2),
|
||||
ATTR_PROBABILITY_THRESHOLD: self._probability_threshold,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
69
homeassistant/components/binary_sensor/gc100.py
Normal file
69
homeassistant/components/binary_sensor/gc100.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Support for binary sensor using GC100.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.gc100/
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.gc100 import DATA_GC100, CONF_PORTS
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['gc100']
|
||||
|
||||
_SENSORS_SCHEMA = vol.Schema({
|
||||
cv.string: cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PORTS): vol.All(cv.ensure_list, [_SENSORS_SCHEMA])
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the GC100 devices."""
|
||||
binary_sensors = []
|
||||
ports = config.get(CONF_PORTS)
|
||||
for port in ports:
|
||||
for port_addr, port_name in port.items():
|
||||
binary_sensors.append(GC100BinarySensor(
|
||||
port_name, port_addr, hass.data[DATA_GC100]))
|
||||
add_devices(binary_sensors, True)
|
||||
|
||||
|
||||
class GC100BinarySensor(BinarySensorDevice):
|
||||
"""Representation of a binary sensor from GC100."""
|
||||
|
||||
def __init__(self, name, port_addr, gc100):
|
||||
"""Initialize the GC100 binary sensor."""
|
||||
# pylint: disable=no-member
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._port_addr = port_addr
|
||||
self._gc100 = gc100
|
||||
self._state = None
|
||||
|
||||
# Subscribe to be notified about state changes (PUSH)
|
||||
self._gc100.subscribe(self._port_addr, self.set_state)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update the sensor state."""
|
||||
self._gc100.read_sensor(self._port_addr, self.set_state)
|
||||
|
||||
def set_state(self, state):
|
||||
"""Set the current state."""
|
||||
self._state = state == 1
|
||||
self.schedule_update_ha_state()
|
||||
@@ -55,12 +55,12 @@ class InsteonPLMBinarySensorDevice(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the the address of the node."""
|
||||
"""Return the address of the node."""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the the name of the node."""
|
||||
"""Return the name of the node."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
|
||||
@@ -13,7 +13,8 @@ import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME, ATTR_LONGITUDE, ATTR_LATITUDE)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, ATTR_LONGITUDE, ATTR_LATITUDE, CONF_SHOW_ON_MAP)
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pyiss==1.0.1']
|
||||
@@ -23,8 +24,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_ISS_NEXT_RISE = 'next_rise'
|
||||
ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space'
|
||||
|
||||
CONF_SHOW_ON_MAP = 'show_on_map'
|
||||
|
||||
DEFAULT_NAME = 'ISS'
|
||||
DEFAULT_DEVICE_CLASS = 'visible'
|
||||
|
||||
|
||||
96
homeassistant/components/binary_sensor/linode.py
Normal file
96
homeassistant/components/binary_sensor/linode.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Support for monitoring the state of Linode Nodes.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.linode/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.linode import (
|
||||
CONF_NODES, ATTR_CREATED, ATTR_NODE_ID, ATTR_NODE_NAME,
|
||||
ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||
ATTR_REGION, ATTR_VCPUS, DATA_LINODE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Node'
|
||||
DEFAULT_DEVICE_CLASS = 'moving'
|
||||
DEPENDENCIES = ['linode']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NODES): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Linode droplet sensor."""
|
||||
linode = hass.data.get(DATA_LINODE)
|
||||
nodes = config.get(CONF_NODES)
|
||||
|
||||
dev = []
|
||||
for node in nodes:
|
||||
node_id = linode.get_node_id(node)
|
||||
if node_id is None:
|
||||
_LOGGER.error("Node %s is not available", node)
|
||||
return
|
||||
dev.append(LinodeBinarySensor(linode, node_id))
|
||||
|
||||
add_devices(dev, True)
|
||||
|
||||
|
||||
class LinodeBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Linode droplet sensor."""
|
||||
|
||||
def __init__(self, li, node_id):
|
||||
"""Initialize a new Linode sensor."""
|
||||
self._linode = li
|
||||
self._node_id = node_id
|
||||
self._state = None
|
||||
self.data = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
if self.data is not None:
|
||||
return self.data.label
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
if self.data is not None:
|
||||
return self.data.status == 'running'
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEFAULT_DEVICE_CLASS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the Linode Node."""
|
||||
if self.data:
|
||||
return {
|
||||
ATTR_CREATED: self.data.created,
|
||||
ATTR_NODE_ID: self.data.id,
|
||||
ATTR_NODE_NAME: self.data.label,
|
||||
ATTR_IPV4_ADDRESS: self.data.ipv4,
|
||||
ATTR_IPV6_ADDRESS: self.data.ipv6,
|
||||
ATTR_MEMORY: self.data.specs.memory,
|
||||
ATTR_REGION: self.data.region.country,
|
||||
ATTR_VCPUS: self.data.specs.vcpus,
|
||||
}
|
||||
return {}
|
||||
|
||||
def update(self):
|
||||
"""Update state of sensor."""
|
||||
self._linode.update()
|
||||
if self._linode.data is not None:
|
||||
for node in self._linode.data:
|
||||
if node.id == self._node_id:
|
||||
self.data = node
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.netatmo import CameraData
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import CONF_TIMEOUT, CONF_OFFSET
|
||||
from homeassistant.const import CONF_TIMEOUT
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -44,14 +44,12 @@ CONF_WELCOME_SENSORS = 'welcome_sensors'
|
||||
CONF_PRESENCE_SENSORS = 'presence_sensors'
|
||||
CONF_TAG_SENSORS = 'tag_sensors'
|
||||
|
||||
DEFAULT_TIMEOUT = 15
|
||||
DEFAULT_OFFSET = 90
|
||||
DEFAULT_TIMEOUT = 90
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): cv.positive_int,
|
||||
vol.Optional(CONF_PRESENCE_SENSORS, default=PRESENCE_SENSOR_TYPES):
|
||||
vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]),
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
@@ -66,7 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
offset = config.get(CONF_OFFSET)
|
||||
if timeout is None:
|
||||
timeout = DEFAULT_TIMEOUT
|
||||
|
||||
module_name = None
|
||||
|
||||
@@ -94,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for variable in welcome_sensors:
|
||||
add_devices([NetatmoBinarySensor(
|
||||
data, camera_name, module_name, home, timeout,
|
||||
offset, camera_type, variable)], True)
|
||||
camera_type, variable)], True)
|
||||
if camera_type == 'NOC':
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
@@ -102,14 +101,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
continue
|
||||
for variable in presence_sensors:
|
||||
add_devices([NetatmoBinarySensor(
|
||||
data, camera_name, module_name, home, timeout, offset,
|
||||
data, camera_name, module_name, home, timeout,
|
||||
camera_type, variable)], True)
|
||||
|
||||
for module_name in data.get_module_names(camera_name):
|
||||
for variable in tag_sensors:
|
||||
camera_type = None
|
||||
add_devices([NetatmoBinarySensor(
|
||||
data, camera_name, module_name, home, timeout, offset,
|
||||
data, camera_name, module_name, home, timeout,
|
||||
camera_type, variable)], True)
|
||||
|
||||
|
||||
@@ -117,14 +116,13 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
"""Represent a single binary sensor in a Netatmo Camera device."""
|
||||
|
||||
def __init__(self, data, camera_name, module_name, home,
|
||||
timeout, offset, camera_type, sensor):
|
||||
timeout, camera_type, sensor):
|
||||
"""Set up for access to the Netatmo camera events."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._module_name = module_name
|
||||
self._home = home
|
||||
self._timeout = timeout
|
||||
self._offset = offset
|
||||
if home:
|
||||
self._name = '{} / {}'.format(home, camera_name)
|
||||
else:
|
||||
@@ -173,40 +171,39 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
self._data.camera_data.someoneKnownSeen(
|
||||
self._home, self._camera_name, self._timeout*60)
|
||||
self._home, self._camera_name, self._timeout)
|
||||
elif self._sensor_name == "Someone unknown":
|
||||
self._state =\
|
||||
self._data.camera_data.someoneUnknownSeen(
|
||||
self._home, self._camera_name, self._timeout*60)
|
||||
self._home, self._camera_name, self._timeout)
|
||||
elif self._sensor_name == "Motion":
|
||||
self._state =\
|
||||
self._data.camera_data.motionDetected(
|
||||
self._home, self._camera_name, self._timeout*60)
|
||||
self._home, self._camera_name, self._timeout)
|
||||
elif self._cameratype == 'NOC':
|
||||
if self._sensor_name == "Outdoor motion":
|
||||
self._state =\
|
||||
self._data.camera_data.outdoormotionDetected(
|
||||
self._home, self._camera_name, self._offset)
|
||||
self._home, self._camera_name, self._timeout)
|
||||
elif self._sensor_name == "Outdoor human":
|
||||
self._state =\
|
||||
self._data.camera_data.humanDetected(
|
||||
self._home, self._camera_name, self._offset)
|
||||
self._home, self._camera_name, self._timeout)
|
||||
elif self._sensor_name == "Outdoor animal":
|
||||
self._state =\
|
||||
self._data.camera_data.animalDetected(
|
||||
self._home, self._camera_name, self._offset)
|
||||
self._home, self._camera_name, self._timeout)
|
||||
elif self._sensor_name == "Outdoor vehicle":
|
||||
self._state =\
|
||||
self._data.camera_data.carDetected(
|
||||
self._home, self._camera_name, self._offset)
|
||||
self._home, self._camera_name, self._timeout)
|
||||
if self._sensor_name == "Tag Vibration":
|
||||
self._state =\
|
||||
self._data.camera_data.moduleMotionDetected(
|
||||
self._home, self._module_name, self._camera_name,
|
||||
self._timeout*60)
|
||||
self._timeout)
|
||||
elif self._sensor_name == "Tag Open":
|
||||
self._state =\
|
||||
self._data.camera_data.moduleOpened(
|
||||
self._home, self._module_name, self._camera_name)
|
||||
else:
|
||||
return None
|
||||
self._home, self._module_name, self._camera_name,
|
||||
self._timeout)
|
||||
|
||||
72
homeassistant/components/binary_sensor/raincloud.py
Normal file
72
homeassistant/components/binary_sensor/raincloud.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Support for Melnor RainCloud sprinkler water timer.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.raincloud/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.raincloud import (
|
||||
BINARY_SENSORS, DATA_RAINCLOUD, ICON_MAP, RainCloudEntity)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
|
||||
DEPENDENCIES = ['raincloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for a raincloud device."""
|
||||
raincloud = hass.data[DATA_RAINCLOUD].data
|
||||
|
||||
sensors = []
|
||||
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||
if sensor_type == 'status':
|
||||
sensors.append(
|
||||
RainCloudBinarySensor(raincloud.controller, sensor_type))
|
||||
sensors.append(
|
||||
RainCloudBinarySensor(raincloud.controller.faucet,
|
||||
sensor_type))
|
||||
|
||||
else:
|
||||
# create an sensor for each zone managed by faucet
|
||||
for zone in raincloud.controller.faucet.zones:
|
||||
sensors.append(RainCloudBinarySensor(zone, sensor_type))
|
||||
|
||||
add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice):
|
||||
"""A sensor implementation for raincloud device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
_LOGGER.debug("Updating RainCloud sensor: %s", self._name)
|
||||
self._state = getattr(self.data, self._sensor_type)
|
||||
if self._sensor_type == 'status':
|
||||
self._state = self._state == 'Online'
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of this device."""
|
||||
if self._sensor_type == 'is_watering':
|
||||
return 'mdi:water' if self.is_on else 'mdi:water-off'
|
||||
elif self._sensor_type == 'status':
|
||||
return 'mdi:pipe' if self.is_on else 'mdi:pipe-disconnected'
|
||||
return ICON_MAP.get(self._sensor_type)
|
||||
64
homeassistant/components/binary_sensor/random.py
Normal file
64
homeassistant/components/binary_sensor/random.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Support for showing random states.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.random/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Random Binary Sensor'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Random binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_devices([RandomSensor(name, device_class)], True)
|
||||
|
||||
|
||||
class RandomSensor(BinarySensorDevice):
|
||||
"""Representation of a Random binary sensor."""
|
||||
|
||||
def __init__(self, name, device_class):
|
||||
"""Initialize the Random binary sensor."""
|
||||
self._name = name
|
||||
self._device_class = device_class
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get new state and update the sensor's state."""
|
||||
from random import getrandbits
|
||||
self._state = bool(getrandbits(1))
|
||||
@@ -62,7 +62,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
entity[CONF_COMMAND_ON],
|
||||
entity[CONF_COMMAND_OFF])
|
||||
device.hass = hass
|
||||
device.is_lighting4 = (packet_id[2:4] == '13')
|
||||
sensors.append(device)
|
||||
rfxtrx.RFX_DEVICES[device_id] = device
|
||||
|
||||
@@ -86,17 +85,16 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
if not config[ATTR_AUTOMATIC_ADD]:
|
||||
return
|
||||
|
||||
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
|
||||
|
||||
if poss_dev is not None:
|
||||
poss_id = slugify(poss_dev.event.device.id_string.lower())
|
||||
_LOGGER.info("Found possible matching deviceid %s.",
|
||||
poss_id)
|
||||
if event.device.packettype == 0x13:
|
||||
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
|
||||
if poss_dev is not None:
|
||||
poss_id = slugify(poss_dev.event.device.id_string.lower())
|
||||
_LOGGER.info("Found possible matching deviceid %s.",
|
||||
poss_id)
|
||||
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
sensor = RfxtrxBinarySensor(event, pkt_id)
|
||||
sensor.hass = hass
|
||||
sensor.is_lighting4 = (pkt_id[2:4] == '13')
|
||||
rfxtrx.RFX_DEVICES[device_id] = sensor
|
||||
add_devices_callback([sensor])
|
||||
_LOGGER.info("Added binary sensor %s "
|
||||
@@ -114,6 +112,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype)
|
||||
|
||||
if sensor.is_lighting4:
|
||||
if sensor.data_bits is not None:
|
||||
cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits)
|
||||
@@ -154,7 +153,7 @@ class RfxtrxBinarySensor(BinarySensorDevice):
|
||||
self._device_class = device_class
|
||||
self._off_delay = off_delay
|
||||
self._state = False
|
||||
self.is_lighting4 = False
|
||||
self.is_lighting4 = (event.device.packettype == 0x13)
|
||||
self.delay_listener = None
|
||||
self._data_bits = data_bits
|
||||
self._cmd_on = cmd_on
|
||||
|
||||
@@ -11,7 +11,7 @@ import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.ring import (
|
||||
CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE)
|
||||
CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DATA_RING)
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS)
|
||||
@@ -27,21 +27,21 @@ SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
# Sensor types: Name, category, device_class
|
||||
SENSOR_TYPES = {
|
||||
'ding': ['Ding', ['doorbell'], 'occupancy'],
|
||||
'motion': ['Motion', ['doorbell'], 'motion'],
|
||||
'ding': ['Ding', ['doorbell', 'stickup_cams'], 'occupancy'],
|
||||
'motion': ['Motion', ['doorbell', 'stickup_cams'], 'motion'],
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE):
|
||||
cv.string,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for a Ring device."""
|
||||
ring = hass.data.get('ring')
|
||||
ring = hass.data[DATA_RING]
|
||||
|
||||
sensors = []
|
||||
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||
@@ -50,6 +50,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
sensors.append(RingBinarySensor(hass,
|
||||
device,
|
||||
sensor_type))
|
||||
|
||||
for device in ring.stickup_cams:
|
||||
if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]:
|
||||
sensors.append(RingBinarySensor(hass,
|
||||
device,
|
||||
sensor_type))
|
||||
add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
97
homeassistant/components/binary_sensor/skybell.py
Normal file
97
homeassistant/components/binary_sensor/skybell.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Binary sensor support for the Skybell HD Doorbell.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.skybell/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.skybell import (
|
||||
DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['skybell']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
# Sensor types: Name, device_class, event
|
||||
SENSOR_TYPES = {
|
||||
'button': ['Button', 'occupancy', 'device:sensor:button'],
|
||||
'motion': ['Motion', 'motion', 'device:sensor:motion'],
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE):
|
||||
cv.string,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the platform for a Skybell device."""
|
||||
skybell = hass.data.get(SKYBELL_DOMAIN)
|
||||
|
||||
sensors = []
|
||||
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||
for device in skybell.get_devices():
|
||||
sensors.append(SkybellBinarySensor(device, sensor_type))
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class SkybellBinarySensor(SkybellDevice, BinarySensorDevice):
|
||||
"""A binary sensor implementation for Skybell devices."""
|
||||
|
||||
def __init__(self, device, sensor_type):
|
||||
"""Initialize a binary sensor for a Skybell device."""
|
||||
super().__init__(device)
|
||||
self._sensor_type = sensor_type
|
||||
self._name = "{0} {1}".format(self._device.name,
|
||||
SENSOR_TYPES[self._sensor_type][0])
|
||||
self._device_class = SENSOR_TYPES[self._sensor_type][1]
|
||||
self._event = {}
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = super().device_state_attributes
|
||||
|
||||
attrs['event_date'] = self._event.get('createdAt')
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
super().update()
|
||||
|
||||
event = self._device.latest(SENSOR_TYPES[self._sensor_type][2])
|
||||
|
||||
self._state = bool(event and event.get('id') != self._event.get('id'))
|
||||
|
||||
self._event = event
|
||||
34
homeassistant/components/binary_sensor/tellduslive.py
Normal file
34
homeassistant/components/binary_sensor/tellduslive.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Support for binary sensors using Tellstick Net.
|
||||
|
||||
This platform uses the Telldus Live online service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.tellduslive/
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.tellduslive import TelldusLiveEntity
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Tellstick sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
add_devices(
|
||||
TelldusLiveSensor(hass, binary_sensor)
|
||||
for binary_sensor in discovery_info
|
||||
)
|
||||
|
||||
|
||||
class TelldusLiveSensor(TelldusLiveEntity, BinarySensorDevice):
|
||||
"""Representation of a Tellstick sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self.device.is_on
|
||||
@@ -15,13 +15,12 @@ from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START, STATE_ON)
|
||||
CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -94,10 +93,6 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._state = state.state == STATE_ON
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Handle the target device state changes."""
|
||||
@@ -135,7 +130,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
@callback
|
||||
def _async_render(self, *args):
|
||||
def _async_render(self):
|
||||
"""Get the state of template."""
|
||||
try:
|
||||
return self._template.async_render().lower() == 'true'
|
||||
@@ -171,5 +166,5 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
period = self._delay_on if state else self._delay_off
|
||||
async_track_same_state(
|
||||
self.hass, state, period, set_state, entity_ids=self._entities,
|
||||
async_check_func=self._async_render)
|
||||
self.hass, period, set_state, entity_ids=self._entities,
|
||||
async_check_same_func=lambda *args: self._async_render() == state)
|
||||
|
||||
@@ -30,7 +30,6 @@ class TeslaBinarySensor(TeslaDevice, BinarySensorDevice):
|
||||
def __init__(self, tesla_device, controller, sensor_type):
|
||||
"""Initialisation of binary sensor."""
|
||||
super().__init__(tesla_device, controller)
|
||||
self._name = self.tesla_device.name
|
||||
self._state = False
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
self._sensor_type = sensor_type
|
||||
|
||||
@@ -20,15 +20,18 @@ from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_HYSTERESIS = 'hysteresis'
|
||||
ATTR_SENSOR_VALUE = 'sensor_value'
|
||||
ATTR_THRESHOLD = 'threshold'
|
||||
ATTR_TYPE = 'type'
|
||||
|
||||
CONF_HYSTERESIS = 'hysteresis'
|
||||
CONF_LOWER = 'lower'
|
||||
CONF_THRESHOLD = 'threshold'
|
||||
CONF_UPPER = 'upper'
|
||||
|
||||
DEFAULT_NAME = 'Threshold'
|
||||
DEFAULT_HYSTERESIS = 0.0
|
||||
|
||||
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER]
|
||||
|
||||
@@ -36,6 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
|
||||
vol.Optional(
|
||||
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
@@ -47,28 +52,32 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
name = config.get(CONF_NAME)
|
||||
threshold = config.get(CONF_THRESHOLD)
|
||||
hysteresis = config.get(CONF_HYSTERESIS)
|
||||
limit_type = config.get(CONF_TYPE)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_devices(
|
||||
[ThresholdSensor(hass, entity_id, name, threshold, limit_type,
|
||||
device_class)], True)
|
||||
async_add_devices([ThresholdSensor(
|
||||
hass, entity_id, name, threshold,
|
||||
hysteresis, limit_type, device_class)
|
||||
], True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ThresholdSensor(BinarySensorDevice):
|
||||
"""Representation of a Threshold sensor."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, threshold, limit_type,
|
||||
device_class):
|
||||
def __init__(self, hass, entity_id, name, threshold,
|
||||
hysteresis, limit_type, device_class):
|
||||
"""Initialize the Threshold sensor."""
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self.is_upper = limit_type == 'upper'
|
||||
self._name = name
|
||||
self._threshold = threshold
|
||||
self._hysteresis = hysteresis
|
||||
self._device_class = device_class
|
||||
self._deviation = False
|
||||
self._state = False
|
||||
self.sensor_value = 0
|
||||
|
||||
@callback
|
||||
@@ -97,7 +106,7 @@ class ThresholdSensor(BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._deviation
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -116,13 +125,16 @@ class ThresholdSensor(BinarySensorDevice):
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_SENSOR_VALUE: self.sensor_value,
|
||||
ATTR_THRESHOLD: self._threshold,
|
||||
ATTR_HYSTERESIS: self._hysteresis,
|
||||
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
if self.is_upper:
|
||||
self._deviation = bool(self.sensor_value > self._threshold)
|
||||
else:
|
||||
self._deviation = bool(self.sensor_value < self._threshold)
|
||||
if self._hysteresis == 0 and self.sensor_value == self._threshold:
|
||||
self._state = False
|
||||
elif self.sensor_value > (self._threshold + self._hysteresis):
|
||||
self._state = self.is_upper
|
||||
elif self.sensor_value < (self._threshold - self._hysteresis):
|
||||
self._state = not self.is_upper
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""
|
||||
A sensor that monitors trands in other components.
|
||||
A sensor that monitors trends in other components.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.trend/
|
||||
"""
|
||||
import asyncio
|
||||
from collections import deque
|
||||
import logging
|
||||
import math
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -16,21 +18,40 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_DEVICE_CLASS, STATE_UNKNOWN)
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME,
|
||||
CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.13.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ATTRIBUTE = 'attribute'
|
||||
ATTR_GRADIENT = 'gradient'
|
||||
ATTR_MIN_GRADIENT = 'min_gradient'
|
||||
ATTR_INVERT = 'invert'
|
||||
ATTR_SAMPLE_DURATION = 'sample_duration'
|
||||
ATTR_SAMPLE_COUNT = 'sample_count'
|
||||
|
||||
CONF_SENSORS = 'sensors'
|
||||
CONF_ATTRIBUTE = 'attribute'
|
||||
CONF_MAX_SAMPLES = 'max_samples'
|
||||
CONF_MIN_GRADIENT = 'min_gradient'
|
||||
CONF_INVERT = 'invert'
|
||||
CONF_SAMPLE_DURATION = 'sample_duration'
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(CONF_MAX_SAMPLES, default=2): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_GRADIENT, default=0.0): vol.Coerce(float),
|
||||
vol.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SAMPLE_DURATION, default=0): cv.positive_int,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -43,17 +64,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the trend sensors."""
|
||||
sensors = []
|
||||
|
||||
for device, device_config in config[CONF_SENSORS].items():
|
||||
for device_id, device_config in config[CONF_SENSORS].items():
|
||||
entity_id = device_config[ATTR_ENTITY_ID]
|
||||
attribute = device_config.get(CONF_ATTRIBUTE)
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||
device_class = device_config.get(CONF_DEVICE_CLASS)
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device_id)
|
||||
invert = device_config[CONF_INVERT]
|
||||
max_samples = device_config[CONF_MAX_SAMPLES]
|
||||
min_gradient = device_config[CONF_MIN_GRADIENT]
|
||||
sample_duration = device_config[CONF_SAMPLE_DURATION]
|
||||
|
||||
sensors.append(
|
||||
SensorTrend(
|
||||
hass, device, friendly_name, entity_id, attribute,
|
||||
device_class, invert)
|
||||
hass, device_id, friendly_name, entity_id, attribute,
|
||||
device_class, invert, max_samples, min_gradient,
|
||||
sample_duration)
|
||||
)
|
||||
if not sensors:
|
||||
_LOGGER.error("No sensors added")
|
||||
@@ -65,30 +90,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class SensorTrend(BinarySensorDevice):
|
||||
"""Representation of a trend Sensor."""
|
||||
|
||||
def __init__(self, hass, device_id, friendly_name,
|
||||
target_entity, attribute, device_class, invert):
|
||||
def __init__(self, hass, device_id, friendly_name, entity_id,
|
||||
attribute, device_class, invert, max_samples,
|
||||
min_gradient, sample_duration):
|
||||
"""Initialize the sensor."""
|
||||
self._hass = hass
|
||||
self.entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||
self._name = friendly_name
|
||||
self._target_entity = target_entity
|
||||
self._entity_id = entity_id
|
||||
self._attribute = attribute
|
||||
self._device_class = device_class
|
||||
self._invert = invert
|
||||
self._sample_duration = sample_duration
|
||||
self._min_gradient = min_gradient
|
||||
self._gradient = None
|
||||
self._state = None
|
||||
self.from_state = None
|
||||
self.to_state = None
|
||||
|
||||
@callback
|
||||
def trend_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Handle the target device state changes."""
|
||||
self.from_state = old_state
|
||||
self.to_state = new_state
|
||||
hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
track_state_change(hass, target_entity,
|
||||
trend_sensor_state_listener)
|
||||
self.samples = deque(maxlen=max_samples)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -105,33 +123,77 @@ class SensorTrend(BinarySensorDevice):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_FRIENDLY_NAME: self._name,
|
||||
ATTR_INVERT: self._invert,
|
||||
ATTR_GRADIENT: self._gradient,
|
||||
ATTR_MIN_GRADIENT: self._min_gradient,
|
||||
ATTR_SAMPLE_DURATION: self._sample_duration,
|
||||
ATTR_SAMPLE_COUNT: len(self.samples),
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Complete device setup after being added to hass."""
|
||||
@callback
|
||||
def trend_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Handle state changes on the observed device."""
|
||||
try:
|
||||
if self._attribute:
|
||||
state = new_state.attributes.get(self._attribute)
|
||||
else:
|
||||
state = new_state.state
|
||||
if state != STATE_UNKNOWN:
|
||||
sample = (utcnow().timestamp(), float(state))
|
||||
self.samples.append(sample)
|
||||
self.async_schedule_update_ha_state(True)
|
||||
except (ValueError, TypeError) as ex:
|
||||
_LOGGER.error(ex)
|
||||
|
||||
async_track_state_change(
|
||||
self.hass, self._entity_id,
|
||||
trend_sensor_state_listener)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
if self.from_state is None or self.to_state is None:
|
||||
return
|
||||
if (self.from_state.state == STATE_UNKNOWN or
|
||||
self.to_state.state == STATE_UNKNOWN):
|
||||
return
|
||||
try:
|
||||
if self._attribute:
|
||||
from_value = float(
|
||||
self.from_state.attributes.get(self._attribute))
|
||||
to_value = float(
|
||||
self.to_state.attributes.get(self._attribute))
|
||||
else:
|
||||
from_value = float(self.from_state.state)
|
||||
to_value = float(self.to_state.state)
|
||||
# Remove outdated samples
|
||||
if self._sample_duration > 0:
|
||||
cutoff = utcnow().timestamp() - self._sample_duration
|
||||
while self.samples and self.samples[0][0] < cutoff:
|
||||
self.samples.popleft()
|
||||
|
||||
self._state = to_value > from_value
|
||||
if self._invert:
|
||||
self._state = not self._state
|
||||
if len(self.samples) < 2:
|
||||
return
|
||||
|
||||
except (ValueError, TypeError) as ex:
|
||||
self._state = None
|
||||
_LOGGER.error(ex)
|
||||
# Calculate gradient of linear trend
|
||||
yield from self.hass.async_add_job(self._calculate_gradient)
|
||||
|
||||
# Update state
|
||||
self._state = (
|
||||
abs(self._gradient) > abs(self._min_gradient) and
|
||||
math.copysign(self._gradient, self._min_gradient) == self._gradient
|
||||
)
|
||||
|
||||
if self._invert:
|
||||
self._state = not self._state
|
||||
|
||||
def _calculate_gradient(self):
|
||||
"""Compute the linear trend gradient of the current samples.
|
||||
|
||||
This need run inside executor.
|
||||
"""
|
||||
import numpy as np
|
||||
timestamps = np.array([t for t, _ in self.samples])
|
||||
values = np.array([s for _, s in self.samples])
|
||||
coeffs = np.polyfit(timestamps, values, 1)
|
||||
self._gradient = coeffs[0]
|
||||
|
||||
@@ -9,7 +9,6 @@ import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -87,7 +86,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.info("Device isn't a sensor, skipping")
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
@@ -117,6 +116,11 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return super().device_state_attributes
|
||||
|
||||
|
||||
class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Smoke detector."""
|
||||
@@ -124,9 +128,9 @@ class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'test_activated': self.wink.test_activated()
|
||||
}
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['test_activated'] = self.wink.test_activated()
|
||||
return _attributes
|
||||
|
||||
|
||||
class WinkHub(WinkBinarySensorDevice):
|
||||
@@ -135,10 +139,11 @@ class WinkHub(WinkBinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'update needed': self.wink.update_needed(),
|
||||
'firmware version': self.wink.firmware_version()
|
||||
}
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['update_needed'] = self.wink.update_needed()
|
||||
_attributes['firmware_version'] = self.wink.firmware_version()
|
||||
_attributes['pairing_mode'] = self.wink.pairing_mode()
|
||||
return _attributes
|
||||
|
||||
|
||||
class WinkRemote(WinkBinarySensorDevice):
|
||||
@@ -147,12 +152,12 @@ class WinkRemote(WinkBinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'button_on_pressed': self.wink.button_on_pressed(),
|
||||
'button_off_pressed': self.wink.button_off_pressed(),
|
||||
'button_up_pressed': self.wink.button_up_pressed(),
|
||||
'button_down_pressed': self.wink.button_down_pressed()
|
||||
}
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['button_on_pressed'] = self.wink.button_on_pressed()
|
||||
_attributes['button_off_pressed'] = self.wink.button_off_pressed()
|
||||
_attributes['button_up_pressed'] = self.wink.button_up_pressed()
|
||||
_attributes['button_down_pressed'] = self.wink.button_down_pressed()
|
||||
return _attributes
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@@ -166,10 +171,10 @@ class WinkButton(WinkBinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'pressed': self.wink.pressed(),
|
||||
'long_pressed': self.wink.long_pressed()
|
||||
}
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['pressed'] = self.wink.pressed()
|
||||
_attributes['long_pressed'] = self.wink.long_pressed()
|
||||
return _attributes
|
||||
|
||||
|
||||
class WinkGang(WinkBinarySensorDevice):
|
||||
|
||||
@@ -12,6 +12,7 @@ ATTR_OPEN_SINCE = 'Open since'
|
||||
|
||||
MOTION = 'motion'
|
||||
NO_MOTION = 'no_motion'
|
||||
ATTR_LAST_ACTION = 'last_action'
|
||||
ATTR_NO_MOTION_SINCE = 'No motion since'
|
||||
|
||||
DENSITY = 'density'
|
||||
@@ -24,13 +25,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices['binary_sensor']:
|
||||
model = device['model']
|
||||
if model == 'motion':
|
||||
if model in ['motion', 'sensor_motion.aq2']:
|
||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||
elif model == 'sensor_motion.aq2':
|
||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||
elif model == 'magnet':
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'sensor_magnet.aq2':
|
||||
elif model in ['magnet', 'sensor_magnet.aq2']:
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'sensor_wleak.aq1':
|
||||
devices.append(XiaomiWaterLeakSensor(device, gateway))
|
||||
@@ -38,10 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices.append(XiaomiSmokeSensor(device, gateway))
|
||||
elif model == 'natgas':
|
||||
devices.append(XiaomiNatgasSensor(device, gateway))
|
||||
elif model == 'switch':
|
||||
devices.append(XiaomiButton(device, 'Switch', 'status',
|
||||
hass, gateway))
|
||||
elif model == 'sensor_switch.aq2':
|
||||
elif model in ['switch', 'sensor_switch.aq2', 'sensor_switch.aq3']:
|
||||
devices.append(XiaomiButton(device, 'Switch', 'status',
|
||||
hass, gateway))
|
||||
elif model == '86sw1':
|
||||
@@ -288,9 +282,17 @@ class XiaomiButton(XiaomiBinarySensor):
|
||||
def __init__(self, device, name, data_key, hass, xiaomi_hub):
|
||||
"""Initialize the XiaomiButton."""
|
||||
self._hass = hass
|
||||
self._last_action = None
|
||||
XiaomiBinarySensor.__init__(self, device, name, xiaomi_hub,
|
||||
data_key, None)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_LAST_ACTION: self._last_action}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
value = data.get(self._data_key)
|
||||
@@ -316,6 +318,8 @@ class XiaomiButton(XiaomiBinarySensor):
|
||||
'entity_id': self.entity_id,
|
||||
'click_type': click_type
|
||||
})
|
||||
self._last_action = click_type
|
||||
|
||||
if value in ['long_click_press', 'long_click_release']:
|
||||
return True
|
||||
return False
|
||||
@@ -327,10 +331,18 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
def __init__(self, device, hass, xiaomi_hub):
|
||||
"""Initialize the Xiaomi Cube."""
|
||||
self._hass = hass
|
||||
self._last_action = None
|
||||
self._state = False
|
||||
XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub,
|
||||
None, None)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_LAST_ACTION: self._last_action}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
if 'status' in data:
|
||||
@@ -338,6 +350,7 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': data['status']
|
||||
})
|
||||
self._last_action = data['status']
|
||||
|
||||
if 'rotate' in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
@@ -345,4 +358,6 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
'action_type': 'rotate',
|
||||
'action_value': float(data['rotate'].replace(",", "."))
|
||||
})
|
||||
return False
|
||||
self._last_action = 'rotate'
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
# Describes the format for available calendar services
|
||||
|
||||
todoist:
|
||||
new_task:
|
||||
description: Create a new task and add it to a project.
|
||||
fields:
|
||||
content:
|
||||
description: The name of the task. [Required]
|
||||
description: The name of the task (Required).
|
||||
example: Pick up the mail
|
||||
project:
|
||||
description: The name of the project this task should belong to. Defaults to Inbox. [Optional]
|
||||
description: The name of the project this task should belong to. Defaults to Inbox (Optional).
|
||||
example: Errands
|
||||
labels:
|
||||
description: Any labels that you want to apply to this task, separated by a comma. [Optional]
|
||||
description: Any labels that you want to apply to this task, separated by a comma (Optional).
|
||||
example: Chores,Deliveries
|
||||
priority:
|
||||
description: The priority of this task, from 1 (normal) to 4 (urgent). [Optional]
|
||||
description: The priority of this task, from 1 (normal) to 4 (urgent) (Optional).
|
||||
example: 2
|
||||
due_date:
|
||||
description: The day this task is due, in format YYYY-MM-DD. [Optional]
|
||||
description: The day this task is due, in format YYYY-MM-DD (Optional).
|
||||
example: "2018-04-01"
|
||||
|
||||
@@ -277,7 +277,7 @@ class TodoistProjectData(object):
|
||||
"""
|
||||
Class used by the Task Device service object to hold all Todoist Tasks.
|
||||
|
||||
This is analagous to the GoogleCalendarData found in the Google Calendar
|
||||
This is analogous to the GoogleCalendarData found in the Google Calendar
|
||||
component.
|
||||
|
||||
Takes an object with a 'name' field and optionally an 'id' field (either
|
||||
|
||||
@@ -29,18 +29,22 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_EN_MOTION = 'enable_motion_detection'
|
||||
SERVICE_DISEN_MOTION = 'disable_motion_detection'
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
SERVICE_ENABLE_MOTION = 'enable_motion_detection'
|
||||
SERVICE_DISABLE_MOTION = 'disable_motion_detection'
|
||||
SERVICE_SNAPSHOT = 'snapshot'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
ATTR_FILENAME = 'filename'
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
@@ -55,13 +59,17 @@ CAMERA_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
|
||||
vol.Required(ATTR_FILENAME): cv.template
|
||||
})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def enable_motion_detection(hass, entity_id=None):
|
||||
"""Enable Motion Detection."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_EN_MOTION, data))
|
||||
DOMAIN, SERVICE_ENABLE_MOTION, data))
|
||||
|
||||
|
||||
@bind_hass
|
||||
@@ -69,9 +77,20 @@ def disable_motion_detection(hass, entity_id=None):
|
||||
"""Disable Motion Detection."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_DISEN_MOTION, data))
|
||||
DOMAIN, SERVICE_DISABLE_MOTION, data))
|
||||
|
||||
|
||||
@bind_hass
|
||||
def async_snapshot(hass, filename, entity_id=None):
|
||||
"""Make a snapshot from a camera."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
data[ATTR_FILENAME] = filename
|
||||
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_SNAPSHOT, data))
|
||||
|
||||
|
||||
@bind_hass
|
||||
@asyncio.coroutine
|
||||
def async_get_image(hass, entity_id, timeout=10):
|
||||
"""Fetch a image from a camera entity."""
|
||||
@@ -119,44 +138,72 @@ def async_setup(hass, config):
|
||||
entity.async_update_token()
|
||||
hass.async_add_job(entity.async_update_ha_state())
|
||||
|
||||
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL)
|
||||
hass.helpers.event.async_track_time_interval(
|
||||
update_tokens, TOKEN_CHANGE_INTERVAL)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_camera_service(service):
|
||||
"""Handle calls to the camera services."""
|
||||
target_cameras = component.async_extract_from_service(service)
|
||||
|
||||
for camera in target_cameras:
|
||||
if service.service == SERVICE_EN_MOTION:
|
||||
yield from camera.async_enable_motion_detection()
|
||||
elif service.service == SERVICE_DISEN_MOTION:
|
||||
yield from camera.async_disable_motion_detection()
|
||||
|
||||
update_tasks = []
|
||||
for camera in target_cameras:
|
||||
if service.service == SERVICE_ENABLE_MOTION:
|
||||
yield from camera.async_enable_motion_detection()
|
||||
elif service.service == SERVICE_DISABLE_MOTION:
|
||||
yield from camera.async_disable_motion_detection()
|
||||
|
||||
if not camera.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.async_add_job(
|
||||
camera.async_update_ha_state(True))
|
||||
if hasattr(camera, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
update_tasks.append(camera.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_snapshot_service(service):
|
||||
"""Handle snapshot services calls."""
|
||||
target_cameras = component.async_extract_from_service(service)
|
||||
filename = service.data[ATTR_FILENAME]
|
||||
filename.hass = hass
|
||||
|
||||
for camera in target_cameras:
|
||||
snapshot_file = filename.async_render(
|
||||
variables={ATTR_ENTITY_ID: camera})
|
||||
|
||||
# check if we allow to access to that file
|
||||
if not hass.config.is_allowed_path(snapshot_file):
|
||||
_LOGGER.error(
|
||||
"Can't write %s, no access to path!", snapshot_file)
|
||||
continue
|
||||
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
def _write_image(to_file, image_data):
|
||||
"""Executor helper to write image."""
|
||||
with open(to_file, 'wb') as img_file:
|
||||
img_file.write(image_data)
|
||||
|
||||
try:
|
||||
yield from hass.async_add_job(
|
||||
_write_image, snapshot_file, image)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write image to file: %s", err)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_EN_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_EN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_ENABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DISEN_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_DISEN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_DISABLE_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_DISABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SNAPSHOT, async_handle_snapshot_service,
|
||||
descriptions.get(SERVICE_SNAPSHOT),
|
||||
schema=CAMERA_SERVICE_SNAPSHOT)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class AmcrestCam(Camera):
|
||||
self._token = self._auth = authentication
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
"""Return a still image response from the camera."""
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = self._camera.snapshot(channel=self._resolution)
|
||||
return response.data
|
||||
|
||||
@@ -1,30 +1,50 @@
|
||||
"""
|
||||
This component provides basic support for Netgear Arlo IP cameras.
|
||||
Support for Netgear Arlo IP cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
|
||||
ATTR_BRIGHTNESS = 'brightness'
|
||||
ATTR_FLIPPED = 'flipped'
|
||||
ATTR_MIRRORED = 'mirrored'
|
||||
ATTR_MOTION = 'motion_detection_sensitivity'
|
||||
ATTR_POWERSAVE = 'power_save_mode'
|
||||
ATTR_SIGNAL_STRENGTH = 'signal_strength'
|
||||
ATTR_UNSEEN_VIDEOS = 'unseen_videos'
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
|
||||
POWERSAVE_MODE_MAPPING = {
|
||||
1: 'best_battery_life',
|
||||
2: 'optimized',
|
||||
3: 'best_video'
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS):
|
||||
cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -53,6 +73,7 @@ class ArloCam(Camera):
|
||||
self._motion_status = False
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self.attrs = {}
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
@@ -80,14 +101,28 @@ class ArloCam(Camera):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self.attrs.get(ATTR_BATTERY_LEVEL),
|
||||
ATTR_BRIGHTNESS: self.attrs.get(ATTR_BRIGHTNESS),
|
||||
ATTR_FLIPPED: self.attrs.get(ATTR_FLIPPED),
|
||||
ATTR_MIRRORED: self.attrs.get(ATTR_MIRRORED),
|
||||
ATTR_MOTION: self.attrs.get(ATTR_MOTION),
|
||||
ATTR_POWERSAVE: self.attrs.get(ATTR_POWERSAVE),
|
||||
ATTR_SIGNAL_STRENGTH: self.attrs.get(ATTR_SIGNAL_STRENGTH),
|
||||
ATTR_UNSEEN_VIDEOS: self.attrs.get(ATTR_UNSEEN_VIDEOS),
|
||||
}
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Camera model."""
|
||||
"""Return the camera model."""
|
||||
return self._camera.model_id
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Camera brand."""
|
||||
"""Return the camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
@property
|
||||
@@ -97,7 +132,7 @@ class ArloCam(Camera):
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Camera Motion Detection Status."""
|
||||
"""Return the camera motion detection status."""
|
||||
return self._motion_status
|
||||
|
||||
def set_base_station_mode(self, mode):
|
||||
@@ -105,7 +140,7 @@ class ArloCam(Camera):
|
||||
# Get the list of base stations identified by library
|
||||
base_stations = self.hass.data[DATA_ARLO].base_stations
|
||||
|
||||
# Some Arlo cameras does not have basestation
|
||||
# Some Arlo cameras does not have base station
|
||||
# So check if there is base station detected first
|
||||
# if yes, then choose the primary base station
|
||||
# Set the mode on the chosen base station
|
||||
@@ -122,3 +157,16 @@ class ArloCam(Camera):
|
||||
"""Disable the motion detection in base station (Disarm)."""
|
||||
self._motion_status = False
|
||||
self.set_base_station_mode(ARLO_MODE_DISARMED)
|
||||
|
||||
def update(self):
|
||||
"""Add an attribute-update task to the executor pool."""
|
||||
self.attrs[ATTR_BATTERY_LEVEL] = self._camera.get_battery_level
|
||||
self.attrs[ATTR_BRIGHTNESS] = self._camera.get_battery_level
|
||||
self.attrs[ATTR_FLIPPED] = self._camera.get_flip_state,
|
||||
self.attrs[ATTR_MIRRORED] = self._camera.get_mirror_state,
|
||||
self.attrs[
|
||||
ATTR_MOTION] = self._camera.get_motion_detection_sensitivity,
|
||||
self.attrs[ATTR_POWERSAVE] = POWERSAVE_MODE_MAPPING[
|
||||
self._camera.get_powersave_mode],
|
||||
self.attrs[ATTR_SIGNAL_STRENGTH] = self._camera.get_signal_strength,
|
||||
self.attrs[ATTR_UNSEEN_VIDEOS] = self._camera.unseen_videos
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.const import (
|
||||
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -52,9 +52,9 @@ class AxisCamera(MjpegCamera):
|
||||
"""Initialize Axis Communications camera component."""
|
||||
super().__init__(hass, config)
|
||||
self.port = port
|
||||
async_dispatcher_connect(hass,
|
||||
DOMAIN + '_' + config[CONF_NAME] + '_new_ip',
|
||||
self._new_ip)
|
||||
dispatcher_connect(hass,
|
||||
DOMAIN + '_' + config[CONF_NAME] + '_new_ip',
|
||||
self._new_ip)
|
||||
|
||||
def _new_ip(self, host):
|
||||
"""Set new IP for video stream."""
|
||||
|
||||
@@ -76,6 +76,6 @@ class BlinkCamera(Camera):
|
||||
return self.data.camera_thumbs[self._name]
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
"""Return a still image response from the camera."""
|
||||
self.request_image()
|
||||
return self.response.content
|
||||
|
||||
@@ -55,9 +55,9 @@ class FFmpegCamera(Camera):
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
||||
|
||||
image = yield from ffmpeg.get_image(
|
||||
image = yield from asyncio.shield(ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments)
|
||||
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -59,7 +59,7 @@ class FoscamCam(Camera):
|
||||
self._password, verbose=False)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
"""Return a still image response from the camera."""
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
# Handle exception if host is not reachable or url failed
|
||||
result, response = self._foscam_session.snap_picture_2()
|
||||
|
||||
@@ -78,9 +78,9 @@ class ONVIFCamera(Camera):
|
||||
ffmpeg = ImageFrame(
|
||||
self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
|
||||
|
||||
image = yield from ffmpeg.get_image(
|
||||
image = yield from asyncio.shield(ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._ffmpeg_arguments)
|
||||
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
141
homeassistant/components/camera/ring.py
Normal file
141
homeassistant/components/camera/ring.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
This component provides support to the Ring Door Bell camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.ring/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.ring import DATA_RING, CONF_ATTRIBUTION
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEPENDENCIES = ['ring', 'ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=90)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up a Ring Door Bell and StickUp Camera."""
|
||||
ring = hass.data[DATA_RING]
|
||||
|
||||
cams = []
|
||||
for camera in ring.doorbells:
|
||||
cams.append(RingCam(hass, camera, config))
|
||||
|
||||
for camera in ring.stickup_cams:
|
||||
cams.append(RingCam(hass, camera, config))
|
||||
|
||||
async_add_devices(cams, True)
|
||||
return True
|
||||
|
||||
|
||||
class RingCam(Camera):
|
||||
"""An implementation of a Ring Door Bell camera."""
|
||||
|
||||
def __init__(self, hass, camera, device_info):
|
||||
"""Initialize a Ring Door Bell camera."""
|
||||
super(RingCam, self).__init__()
|
||||
self._camera = camera
|
||||
self._hass = hass
|
||||
self._name = self._camera.name
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._last_video_id = self._camera.last_recording_id
|
||||
self._video_url = self._camera.recording_url(self._last_video_id)
|
||||
self._expires_at = None
|
||||
self._utcnow = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._camera.id,
|
||||
'firmware': self._camera.firmware,
|
||||
'kind': self._camera.kind,
|
||||
'timezone': self._camera.timezone,
|
||||
'type': self._camera.family,
|
||||
'video_url': self._video_url,
|
||||
'video_id': self._last_video_id
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
|
||||
if self._video_url is None:
|
||||
return
|
||||
|
||||
image = yield from asyncio.shield(ffmpeg.get_image(
|
||||
self._video_url, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
if self._video_url is None:
|
||||
return
|
||||
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
self._video_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Update the image periodically."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Update camera entity and refresh attributes."""
|
||||
# extract the video expiration from URL
|
||||
x_amz_expires = int(self._video_url.split('&')[0].split('=')[-1])
|
||||
x_amz_date = self._video_url.split('&')[1].split('=')[-1]
|
||||
|
||||
self._utcnow = dt_util.utcnow()
|
||||
self._expires_at = \
|
||||
timedelta(seconds=x_amz_expires) + \
|
||||
dt_util.as_utc(datetime.strptime(x_amz_date, "%Y%m%dT%H%M%SZ"))
|
||||
|
||||
if self._last_video_id != self._camera.last_recording_id:
|
||||
_LOGGER.debug("Updated Ring DoorBell last_video_id")
|
||||
self._last_video_id = self._camera.last_recording_id
|
||||
|
||||
if self._utcnow >= self._expires_at:
|
||||
_LOGGER.debug("Updated Ring DoorBell video_url")
|
||||
self._video_url = self._camera.recording_url(self._last_video_id)
|
||||
@@ -1,17 +1,25 @@
|
||||
# Describes the format for available camera services
|
||||
|
||||
enable_motion_detection:
|
||||
description: Enable the motion detection in a camera
|
||||
|
||||
description: Enable the motion detection in a camera.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to enable motion detection
|
||||
description: Name(s) of entities to enable motion detection.
|
||||
example: 'camera.living_room_camera'
|
||||
|
||||
disable_motion_detection:
|
||||
description: Disable the motion detection in a camera
|
||||
|
||||
description: Disable the motion detection in a camera.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to disable motion detection
|
||||
description: Name(s) of entities to disable motion detection.
|
||||
example: 'camera.living_room_camera'
|
||||
|
||||
snapshot:
|
||||
description: Take a snapshot from a camera.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to create snapshots from.
|
||||
example: 'camera.living_room_camera'
|
||||
filename:
|
||||
description: Template of a Filename. Variable is entity_id.
|
||||
example: '/tmp/snapshot_{{ entity_id }}'
|
||||
|
||||
67
homeassistant/components/camera/skybell.py
Normal file
67
homeassistant/components/camera/skybell.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Camera support for the Skybell HD Doorbell.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.skybell/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.skybell import (
|
||||
DOMAIN as SKYBELL_DOMAIN, SkybellDevice)
|
||||
|
||||
DEPENDENCIES = ['skybell']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=90)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the platform for a Skybell device."""
|
||||
skybell = hass.data.get(SKYBELL_DOMAIN)
|
||||
|
||||
sensors = []
|
||||
for device in skybell.get_devices():
|
||||
sensors.append(SkybellCamera(device))
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class SkybellCamera(SkybellDevice, Camera):
|
||||
"""A camera implementation for Skybell devices."""
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize a camera for a Skybell device."""
|
||||
SkybellDevice.__init__(self, device)
|
||||
Camera.__init__(self)
|
||||
self._name = self._device.name
|
||||
self._url = None
|
||||
self._response = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
def camera_image(self):
|
||||
"""Get the latest camera image."""
|
||||
super().update()
|
||||
|
||||
if self._url != self._device.image:
|
||||
self._url = self._device.image
|
||||
|
||||
try:
|
||||
self._response = requests.get(
|
||||
self._url, stream=True, timeout=10)
|
||||
except requests.HTTPError as err:
|
||||
_LOGGER.warning("Failed to get camera image: %s", err)
|
||||
self._response = None
|
||||
|
||||
if not self._response:
|
||||
return None
|
||||
|
||||
return self._response.content
|
||||
@@ -7,44 +7,25 @@ https://home-assistant.io/components/camera.synology/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_create_clientsession,
|
||||
async_aiohttp_proxy_web)
|
||||
async_aiohttp_proxy_web,
|
||||
async_get_clientsession)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
REQUIREMENTS = ['py-synology==0.1.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
DEFAULT_TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
|
||||
QUERY_CGI = 'query.cgi'
|
||||
QUERY_API = 'SYNO.API.Info'
|
||||
AUTH_API = 'SYNO.API.Auth'
|
||||
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
|
||||
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
|
||||
SESSION_ID = '0'
|
||||
|
||||
WEBAPI_PATH = '/webapi/'
|
||||
AUTH_PATH = 'auth.cgi'
|
||||
CAMERA_PATH = 'camera.cgi'
|
||||
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
SYNO_API_URL = '{0}{1}{2}'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
@@ -62,189 +43,90 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up a Synology IP Camera."""
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||
|
||||
# Determine API to use for authentication
|
||||
syno_api_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
||||
|
||||
query_payload = {
|
||||
'api': QUERY_API,
|
||||
'method': 'Query',
|
||||
'version': '1',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
)
|
||||
|
||||
# Skip content type check because Synology doesn't return JSON with
|
||||
# right content type
|
||||
query_resp = yield from query_req.json(content_type=None)
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_api_url)
|
||||
from synology.surveillance_station import SurveillanceStation
|
||||
surveillance = SurveillanceStation(
|
||||
config.get(CONF_URL),
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
verify_ssl=verify_ssl,
|
||||
timeout=timeout
|
||||
)
|
||||
except (requests.exceptions.RequestException, ValueError):
|
||||
_LOGGER.exception("Error when initializing SurveillanceStation")
|
||||
return False
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||
|
||||
session_id = yield from get_session_id(
|
||||
hass,
|
||||
websession_init,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url,
|
||||
timeout
|
||||
)
|
||||
|
||||
# init websession
|
||||
websession = async_create_clientsession(
|
||||
hass, verify_ssl, cookies={'id': session_id})
|
||||
|
||||
# Use SessionID to get cameras in system
|
||||
syno_camera_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
||||
|
||||
camera_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'List',
|
||||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
camera_req = yield from websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_camera_url)
|
||||
return False
|
||||
|
||||
camera_resp = yield from camera_req.json(content_type=None)
|
||||
cameras = camera_resp['data']['cameras']
|
||||
cameras = surveillance.get_all_cameras()
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
for camera in cameras:
|
||||
if not config.get(CONF_WHITELIST):
|
||||
camera_id = camera['id']
|
||||
snapshot_path = camera['snapshot_path']
|
||||
|
||||
device = SynologyCamera(
|
||||
hass, websession, config, camera_id, camera['name'],
|
||||
snapshot_path, streaming_path, camera_path, auth_path, timeout
|
||||
)
|
||||
device = SynologyCamera(surveillance, camera.camera_id, verify_ssl)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||
"""Get a session id."""
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': username,
|
||||
'passwd': password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
)
|
||||
auth_resp = yield from auth_req.json(content_type=None)
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", login_url)
|
||||
return False
|
||||
|
||||
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
||||
def __init__(self, hass, websession, config, camera_id,
|
||||
camera_name, snapshot_path, streaming_path, camera_path,
|
||||
auth_path, timeout):
|
||||
def __init__(self, surveillance, camera_id, verify_ssl):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._websession = websession
|
||||
self._name = camera_name
|
||||
self._synology_url = config.get(CONF_URL)
|
||||
self._camera_name = config.get(CONF_CAMERA_NAME)
|
||||
self._stream_id = config.get(CONF_STREAM_ID)
|
||||
self._surveillance = surveillance
|
||||
self._camera_id = camera_id
|
||||
self._snapshot_path = snapshot_path
|
||||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
self._timeout = timeout
|
||||
self._verify_ssl = verify_ssl
|
||||
self._camera = self._surveillance.get_camera(camera_id)
|
||||
self._motion_setting = self._surveillance.get_motion_setting(camera_id)
|
||||
self.is_streaming = self._camera.is_enabled
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
image_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._camera_path)
|
||||
|
||||
image_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'GetSnapshot',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||
response = yield from self._websession.get(
|
||||
image_url,
|
||||
params=image_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Error fetching %s", image_url)
|
||||
return None
|
||||
|
||||
image = yield from response.read()
|
||||
|
||||
return image
|
||||
return self._surveillance.get_camera_image(self._camera_id)
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
streaming_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
||||
streaming_url = self._camera.video_stream_url
|
||||
|
||||
streaming_payload = {
|
||||
'api': STREAMING_API,
|
||||
'method': 'Stream',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'
|
||||
}
|
||||
stream_coro = self._websession.get(
|
||||
streaming_url, params=streaming_payload)
|
||||
websession = async_get_clientsession(self.hass, self._verify_ssl)
|
||||
stream_coro = websession.get(streaming_url)
|
||||
|
||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._name
|
||||
return self._camera.name
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return self._camera.is_recording
|
||||
|
||||
def should_poll(self):
|
||||
"""Update the recording state periodically."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Update the status of the camera."""
|
||||
self._surveillance.update()
|
||||
self._camera = self._surveillance.get_camera(self._camera.camera_id)
|
||||
self._motion_setting = self._surveillance.get_motion_setting(
|
||||
self._camera.camera_id)
|
||||
self.is_streaming = self._camera.is_enabled
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return self._motion_setting.is_enabled
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in the camera."""
|
||||
self._surveillance.enable_motion_detection(self._camera_id)
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable motion detection in camera."""
|
||||
self._surveillance.disable_motion_detection(self._camera_id)
|
||||
|
||||
137
homeassistant/components/camera/yi.py
Normal file
137
homeassistant/components/camera/yi.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
This component provides support for Xiaomi Cameras (HiSilicon Hi3518e V200).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.yi/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH,
|
||||
CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_BRAND = 'YI Home Camera'
|
||||
DEFAULT_PASSWORD = ''
|
||||
DEFAULT_PATH = '/tmp/sd/record'
|
||||
DEFAULT_PORT = 21
|
||||
DEFAULT_USERNAME = 'root'
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
|
||||
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up a Yi Camera."""
|
||||
_LOGGER.debug('Received configuration: %s', config)
|
||||
async_add_devices([YiCamera(hass, config)], True)
|
||||
|
||||
|
||||
class YiCamera(Camera):
|
||||
"""Define an implementation of a Yi Camera."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize."""
|
||||
super().__init__()
|
||||
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._last_image = None
|
||||
self._last_url = None
|
||||
self._manager = hass.data[DATA_FFMPEG]
|
||||
self._name = config.get(CONF_NAME)
|
||||
self.host = config.get(CONF_HOST)
|
||||
self.port = config.get(CONF_PORT)
|
||||
self.path = config.get(CONF_PATH)
|
||||
self.user = config.get(CONF_USERNAME)
|
||||
self.passwd = config.get(CONF_PASSWORD)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
def get_latest_video_url(self):
|
||||
"""Retrieve the latest video file from the customized Yi FTP server."""
|
||||
from ftplib import FTP, error_perm
|
||||
|
||||
ftp = FTP(self.host)
|
||||
try:
|
||||
ftp.login(self.user, self.passwd)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('There was an error while logging into the camera')
|
||||
_LOGGER.debug(exc)
|
||||
return False
|
||||
|
||||
try:
|
||||
ftp.cwd(self.path)
|
||||
except error_perm as exc:
|
||||
_LOGGER.error('Unable to find path: %s', self.path)
|
||||
_LOGGER.debug(exc)
|
||||
return False
|
||||
|
||||
dirs = [d for d in ftp.nlst() if '.' not in d]
|
||||
if not dirs:
|
||||
_LOGGER.warning("There don't appear to be any uploaded videos")
|
||||
return False
|
||||
|
||||
latest_dir = dirs[-1]
|
||||
ftp.cwd(latest_dir)
|
||||
videos = ftp.nlst()
|
||||
if not videos:
|
||||
_LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
|
||||
return False
|
||||
|
||||
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
|
||||
self.user, self.passwd, self.host, self.port, self.path,
|
||||
latest_dir, videos[-1])
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
|
||||
url = yield from self.hass.async_add_job(self.get_latest_video_url)
|
||||
if url != self._last_url:
|
||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
||||
self._last_image = yield from asyncio.shield(ffmpeg.get_image(
|
||||
url, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
||||
self._last_url = url
|
||||
|
||||
return self._last_image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
self._last_url, extra_cmd=self._extra_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
@@ -44,6 +44,12 @@ STATE_IDLE = 'idle'
|
||||
STATE_AUTO = 'auto'
|
||||
STATE_DRY = 'dry'
|
||||
STATE_FAN_ONLY = 'fan_only'
|
||||
STATE_ECO = 'eco'
|
||||
STATE_ELECTRIC = 'electric'
|
||||
STATE_PERFORMANCE = 'performance'
|
||||
STATE_HIGH_DEMAND = 'high_demand'
|
||||
STATE_HEAT_PUMP = 'heat_pump'
|
||||
STATE_GAS = 'gas'
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
|
||||
ATTR_MAX_TEMP = 'max_temp'
|
||||
@@ -147,7 +153,7 @@ def set_hold_mode(hass, hold_mode, entity_id=None):
|
||||
|
||||
@bind_hass
|
||||
def set_aux_heat(hass, aux_heat, entity_id=None):
|
||||
"""Turn all or specified climate devices auxillary heater on."""
|
||||
"""Turn all or specified climate devices auxiliary heater on."""
|
||||
data = {
|
||||
ATTR_AUX_HEAT: aux_heat
|
||||
}
|
||||
@@ -230,24 +236,6 @@ def async_setup(hass, config):
|
||||
load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_update_climate(target_climate):
|
||||
"""Update climate entity after service stuff."""
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.async_add_job(
|
||||
climate.async_update_ha_state(True))
|
||||
if hasattr(climate, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_away_mode_set_service(service):
|
||||
"""Set away mode on target climate devices."""
|
||||
@@ -255,13 +243,19 @@ def async_setup(hass, config):
|
||||
|
||||
away_mode = service.data.get(ATTR_AWAY_MODE)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if away_mode:
|
||||
yield from climate.async_turn_away_mode_on()
|
||||
else:
|
||||
yield from climate.async_turn_away_mode_off()
|
||||
|
||||
yield from _async_update_climate(target_climate)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
|
||||
@@ -275,10 +269,16 @@ def async_setup(hass, config):
|
||||
|
||||
hold_mode = service.data.get(ATTR_HOLD_MODE)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_hold_mode(hold_mode)
|
||||
|
||||
yield from _async_update_climate(target_climate)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
|
||||
@@ -292,13 +292,19 @@ def async_setup(hass, config):
|
||||
|
||||
aux_heat = service.data.get(ATTR_AUX_HEAT)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if aux_heat:
|
||||
yield from climate.async_turn_aux_heat_on()
|
||||
else:
|
||||
yield from climate.async_turn_aux_heat_off()
|
||||
|
||||
yield from _async_update_climate(target_climate)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
|
||||
@@ -310,6 +316,7 @@ def async_setup(hass, config):
|
||||
"""Set temperature on the target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
kwargs = {}
|
||||
for value, temp in service.data.items():
|
||||
@@ -324,7 +331,12 @@ def async_setup(hass, config):
|
||||
|
||||
yield from climate.async_set_temperature(**kwargs)
|
||||
|
||||
yield from _async_update_climate(target_climate)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
|
||||
@@ -338,10 +350,15 @@ def async_setup(hass, config):
|
||||
|
||||
humidity = service.data.get(ATTR_HUMIDITY)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_humidity(humidity)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
yield from _async_update_climate(target_climate)
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
|
||||
@@ -355,10 +372,15 @@ def async_setup(hass, config):
|
||||
|
||||
fan = service.data.get(ATTR_FAN_MODE)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_fan_mode(fan)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
yield from _async_update_climate(target_climate)
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
|
||||
@@ -372,10 +394,15 @@ def async_setup(hass, config):
|
||||
|
||||
operation_mode = service.data.get(ATTR_OPERATION_MODE)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_operation_mode(operation_mode)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
yield from _async_update_climate(target_climate)
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
|
||||
@@ -389,10 +416,15 @@ def async_setup(hass, config):
|
||||
|
||||
swing_mode = service.data.get(ATTR_SWING_MODE)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_swing_mode(swing_mode)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
yield from _async_update_climate(target_climate)
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
|
||||
@@ -661,22 +693,22 @@ class ClimateDevice(Entity):
|
||||
return self.hass.async_add_job(self.set_hold_mode, hold_mode)
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
"""Turn auxiliary heater on."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on.
|
||||
"""Turn auxiliary heater on.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_aux_heat_on)
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
"""Turn auxiliary heater off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off.
|
||||
"""Turn auxiliary heater off.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
|
||||
@@ -114,7 +114,7 @@ class DemoClimate(ClimateDevice):
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
"""Return true if aux heat is on."""
|
||||
return self._aux
|
||||
|
||||
@property
|
||||
@@ -183,11 +183,11 @@ class DemoClimate(ClimateDevice):
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn away auxillary heater on."""
|
||||
"""Turn auxillary heater on."""
|
||||
self._aux = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
"""Turn auxiliary heater off."""
|
||||
self._aux = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -27,6 +27,7 @@ ATTR_RESUME_ALL = 'resume_all'
|
||||
DEFAULT_RESUME_ALL = False
|
||||
TEMPERATURE_HOLD = 'temp'
|
||||
VACATION_HOLD = 'vacation'
|
||||
AWAY_MODE = 'awayMode'
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
|
||||
@@ -144,20 +145,20 @@ class Thermostat(ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.thermostat['runtime']['actualTemperature'] / 10
|
||||
return self.thermostat['runtime']['actualTemperature'] / 10.0
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
return self.thermostat['runtime']['desiredHeat'] / 10.0
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
return self.thermostat['runtime']['desiredCool'] / 10.0
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -166,9 +167,9 @@ class Thermostat(ClimateDevice):
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return None
|
||||
if self.current_operation == STATE_HEAT:
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
return self.thermostat['runtime']['desiredHeat'] / 10.0
|
||||
elif self.current_operation == STATE_COOL:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
return self.thermostat['runtime']['desiredCool'] / 10.0
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -186,6 +187,11 @@ class Thermostat(ClimateDevice):
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
mode = self._current_hold_mode
|
||||
return None if mode == AWAY_MODE else mode
|
||||
|
||||
@property
|
||||
def _current_hold_mode(self):
|
||||
events = self.thermostat['events']
|
||||
for event in events:
|
||||
if event['running']:
|
||||
@@ -195,8 +201,8 @@ class Thermostat(ClimateDevice):
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# A temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
# A permanent hold from away climate is away_mode
|
||||
return None
|
||||
# A permanent hold from away climate
|
||||
return AWAY_MODE
|
||||
elif event['holdClimateRef'] != "":
|
||||
# Any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
@@ -269,7 +275,7 @@ class Thermostat(ClimateDevice):
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self.current_hold_mode == 'away'
|
||||
return self._current_hold_mode == AWAY_MODE
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
@@ -277,12 +283,17 @@ class Thermostat(ClimateDevice):
|
||||
return 'auxHeat' in self.thermostat['equipmentStatus']
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.set_hold_mode('away')
|
||||
"""Turn away mode on by setting it on away hold indefinitely."""
|
||||
if self._current_hold_mode != AWAY_MODE:
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index, 'away',
|
||||
'indefinite')
|
||||
self.update_without_throttle = True
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.set_hold_mode(None)
|
||||
if self._current_hold_mode == AWAY_MODE:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Set hold mode (away, home, temp, sleep, etc.)."""
|
||||
@@ -299,7 +310,7 @@ class Thermostat(ClimateDevice):
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
else:
|
||||
if hold_mode == TEMPERATURE_HOLD:
|
||||
self.set_temp_hold(int(self.current_temperature))
|
||||
self.set_temp_hold(self.current_temperature)
|
||||
else:
|
||||
self.data.ecobee.set_climate_hold(
|
||||
self.thermostat_index, hold_mode, self.hold_preference())
|
||||
@@ -325,15 +336,11 @@ class Thermostat(ClimateDevice):
|
||||
elif self.current_operation == STATE_COOL:
|
||||
heat_temp = temp - 20
|
||||
cool_temp = temp
|
||||
|
||||
self.data.ecobee.set_hold_temp(self.thermostat_index, cool_temp,
|
||||
heat_temp, self.hold_preference())
|
||||
_LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, "
|
||||
"cool=%s, is=%s", heat_temp, isinstance(
|
||||
heat_temp, (int, float)), cool_temp,
|
||||
isinstance(cool_temp, (int, float)))
|
||||
|
||||
self.update_without_throttle = True
|
||||
else:
|
||||
# In auto mode set temperature between
|
||||
heat_temp = temp - 10
|
||||
cool_temp = temp + 10
|
||||
self.set_auto_temp_hold(heat_temp, cool_temp)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -343,9 +350,9 @@ class Thermostat(ClimateDevice):
|
||||
|
||||
if self.current_operation == STATE_AUTO and low_temp is not None \
|
||||
and high_temp is not None:
|
||||
self.set_auto_temp_hold(int(low_temp), int(high_temp))
|
||||
self.set_auto_temp_hold(low_temp, high_temp)
|
||||
elif temp is not None:
|
||||
self.set_temp_hold(int(temp))
|
||||
self.set_temp_hold(temp)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Missing valid arguments for set_temperature in %s", kwargs)
|
||||
@@ -364,7 +371,7 @@ class Thermostat(ClimateDevice):
|
||||
def resume_program(self, resume_all):
|
||||
"""Resume the thermostat schedule program."""
|
||||
self.data.ecobee.resume_program(
|
||||
self.thermostat_index, str(resume_all).lower())
|
||||
self.thermostat_index, 'true' if resume_all else 'false')
|
||||
self.update_without_throttle = True
|
||||
|
||||
def hold_preference(self):
|
||||
|
||||
117
homeassistant/components/climate/ephember.py
Normal file
117
homeassistant/components/climate/ephember.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Support for the EPH Controls Ember themostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.ephember/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyephember==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the ephember thermostat."""
|
||||
from pyephember.pyephember import EphEmber
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
try:
|
||||
ember = EphEmber(username, password)
|
||||
zones = ember.get_zones()
|
||||
for zone in zones:
|
||||
add_devices([EphEmberThermostat(ember, zone)])
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Cannot connect to EphEmber")
|
||||
return
|
||||
|
||||
return
|
||||
|
||||
|
||||
class EphEmberThermostat(ClimateDevice):
|
||||
"""Representation of a HeatmiserV3 thermostat."""
|
||||
|
||||
def __init__(self, ember, zone):
|
||||
"""Initialize the thermostat."""
|
||||
self._ember = ember
|
||||
self._zone_name = zone['name']
|
||||
self._zone = zone
|
||||
self._hot_water = zone['isHotWater']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the thermostat, if any."""
|
||||
return self._zone_name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._zone['currentTemperature']
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._zone['targetTemperature']
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self._zone['isCurrentlyActive']:
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if aux heater."""
|
||||
return self._zone['isBoostActive']
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxiliary heater on."""
|
||||
self._ember.activate_boost_by_name(
|
||||
self._zone_name, self._zone['targetTemperature'])
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxiliary heater off."""
|
||||
self._ember.deactivate_boost_by_name(self._zone_name)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
return
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._zone['targetTemperature']
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._zone['targetTemperature']
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
self._zone = self._ember.get_zone(self._zone_name)
|
||||
@@ -17,7 +17,7 @@ from homeassistant.const import (
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.5']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -164,4 +164,8 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
self._thermostat.update()
|
||||
from bluepy.btle import BTLEException
|
||||
try:
|
||||
self._thermostat.update()
|
||||
except BTLEException as ex:
|
||||
_LOGGER.warning("Updating the state failed: %s", ex)
|
||||
|
||||
@@ -36,7 +36,8 @@ CONF_MAX_TEMP = 'max_temp'
|
||||
CONF_TARGET_TEMP = 'target_temp'
|
||||
CONF_AC_MODE = 'ac_mode'
|
||||
CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_TOLERANCE = 'tolerance'
|
||||
CONF_COLD_TOLERANCE = 'cold_tolerance'
|
||||
CONF_HOT_TOLERANCE = 'hot_tolerance'
|
||||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
|
||||
|
||||
@@ -48,7 +49,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
|
||||
vol.Optional(CONF_COLD_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(
|
||||
float),
|
||||
vol.Optional(CONF_HOT_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(
|
||||
float),
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_KEEP_ALIVE): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
@@ -66,12 +70,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
target_temp = config.get(CONF_TARGET_TEMP)
|
||||
ac_mode = config.get(CONF_AC_MODE)
|
||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
tolerance = config.get(CONF_TOLERANCE)
|
||||
cold_tolerance = config.get(CONF_COLD_TOLERANCE)
|
||||
hot_tolerance = config.get(CONF_HOT_TOLERANCE)
|
||||
keep_alive = config.get(CONF_KEEP_ALIVE)
|
||||
|
||||
async_add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance, keep_alive)])
|
||||
target_temp, ac_mode, min_cycle_duration, cold_tolerance,
|
||||
hot_tolerance, keep_alive)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
@@ -79,14 +85,15 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
tolerance, keep_alive):
|
||||
cold_tolerance, hot_tolerance, keep_alive):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.heater_entity_id = heater_entity_id
|
||||
self.ac_mode = ac_mode
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
self._cold_tolerance = cold_tolerance
|
||||
self._hot_tolerance = hot_tolerance
|
||||
self._keep_alive = keep_alive
|
||||
self._enabled = True
|
||||
|
||||
@@ -261,25 +268,29 @@ class GenericThermostat(ClimateDevice):
|
||||
if self.ac_mode:
|
||||
is_cooling = self._is_device_active
|
||||
if is_cooling:
|
||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
too_cold = self._target_temp - self._cur_temp >= \
|
||||
self._cold_tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||
too_hot = self._cur_temp - self._target_temp >= \
|
||||
self._hot_tolerance
|
||||
if too_hot:
|
||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
is_heating = self._is_device_active
|
||||
if is_heating:
|
||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||
too_hot = self._cur_temp - self._target_temp >= \
|
||||
self._hot_tolerance
|
||||
if too_hot:
|
||||
_LOGGER.info('Turning off heater %s',
|
||||
self.heater_entity_id)
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
too_cold = self._target_temp - self._cur_temp >= \
|
||||
self._cold_tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
|
||||
@@ -11,16 +11,15 @@ import datetime
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, ATTR_FAN_MODE, ATTR_FAN_LIST,
|
||||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
ATTR_TEMPERATURE, CONF_REGION)
|
||||
|
||||
REQUIREMENTS = ['evohomeclient==0.2.5',
|
||||
'somecomfort==0.4.1']
|
||||
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.4.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,7 +30,6 @@ ATTR_CURRENT_OPERATION = 'equipment_output_status'
|
||||
CONF_AWAY_TEMPERATURE = 'away_temperature'
|
||||
CONF_COOL_AWAY_TEMPERATURE = 'away_cool_temperature'
|
||||
CONF_HEAT_AWAY_TEMPERATURE = 'away_heat_temperature'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE = 16
|
||||
DEFAULT_COOL_AWAY_TEMPERATURE = 30
|
||||
|
||||
@@ -14,6 +14,8 @@ from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_SETPOINT_ADDRESS = 'setpoint_address'
|
||||
CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address'
|
||||
CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address'
|
||||
CONF_TEMPERATURE_ADDRESS = 'temperature_address'
|
||||
CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address'
|
||||
CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
|
||||
@@ -33,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SETPOINT_ADDRESS): cv.string,
|
||||
vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
|
||||
vol.Required(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
|
||||
@@ -82,6 +86,10 @@ def async_add_devices_config(hass, config, async_add_devices):
|
||||
CONF_TARGET_TEMPERATURE_ADDRESS),
|
||||
group_address_setpoint=config.get(
|
||||
CONF_SETPOINT_ADDRESS),
|
||||
group_address_setpoint_shift=config.get(
|
||||
CONF_SETPOINT_SHIFT_ADDRESS),
|
||||
group_address_setpoint_shift_state=config.get(
|
||||
CONF_SETPOINT_SHIFT_STATE_ADDRESS),
|
||||
group_address_operation_mode=config.get(
|
||||
CONF_OPERATION_MODE_ADDRESS),
|
||||
group_address_operation_mode_state=config.get(
|
||||
@@ -140,13 +148,29 @@ class KNXClimate(ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.device.temperature
|
||||
return self.device.temperature.value
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.device.supports_target_temperature:
|
||||
return self.device.target_temperature
|
||||
return self.device.target_temperature_comfort
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
if self.device.target_temperature_comfort:
|
||||
return max(
|
||||
self.device.target_temperature_comfort,
|
||||
self.device.target_temperature.value)
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
if self.device.target_temperature_comfort:
|
||||
return min(
|
||||
self.device.target_temperature_comfort,
|
||||
self.device.target_temperature.value)
|
||||
return None
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -155,8 +179,8 @@ class KNXClimate(ClimateDevice):
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
if self.device.supports_target_temperature:
|
||||
yield from self.device.set_target_temperature(temperature)
|
||||
yield from self.device.set_target_temperature_comfort(temperature)
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
485
homeassistant/components/climate/mqtt.py
Normal file
485
homeassistant/components/climate/mqtt.py
Normal file
@@ -0,0 +1,485 @@
|
||||
"""
|
||||
Support for MQTT climate devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/climate.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,
|
||||
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO,
|
||||
ATTR_OPERATION_MODE)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
|
||||
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN,
|
||||
MQTT_BASE_PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
DEFAULT_NAME = 'MQTT HVAC'
|
||||
|
||||
CONF_POWER_COMMAND_TOPIC = 'power_command_topic'
|
||||
CONF_POWER_STATE_TOPIC = 'power_state_topic'
|
||||
CONF_MODE_COMMAND_TOPIC = 'mode_command_topic'
|
||||
CONF_MODE_STATE_TOPIC = 'mode_state_topic'
|
||||
CONF_TEMPERATURE_COMMAND_TOPIC = 'temperature_command_topic'
|
||||
CONF_TEMPERATURE_STATE_TOPIC = 'temperature_state_topic'
|
||||
CONF_FAN_MODE_COMMAND_TOPIC = 'fan_mode_command_topic'
|
||||
CONF_FAN_MODE_STATE_TOPIC = 'fan_mode_state_topic'
|
||||
CONF_SWING_MODE_COMMAND_TOPIC = 'swing_mode_command_topic'
|
||||
CONF_SWING_MODE_STATE_TOPIC = 'swing_mode_state_topic'
|
||||
CONF_AWAY_MODE_COMMAND_TOPIC = 'away_mode_command_topic'
|
||||
CONF_AWAY_MODE_STATE_TOPIC = 'away_mode_state_topic'
|
||||
CONF_HOLD_COMMAND_TOPIC = 'hold_command_topic'
|
||||
CONF_HOLD_STATE_TOPIC = 'hold_state_topic'
|
||||
CONF_AUX_COMMAND_TOPIC = 'aux_command_topic'
|
||||
CONF_AUX_STATE_TOPIC = 'aux_state_topic'
|
||||
|
||||
CONF_CURRENT_TEMPERATURE_TOPIC = 'current_temperature_topic'
|
||||
|
||||
CONF_PAYLOAD_ON = 'payload_on'
|
||||
CONF_PAYLOAD_OFF = 'payload_off'
|
||||
|
||||
CONF_FAN_MODE_LIST = 'fan_modes'
|
||||
CONF_MODE_LIST = 'modes'
|
||||
CONF_SWING_MODE_LIST = 'swing_modes'
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_SEND_IF_OFF = 'send_if_off'
|
||||
|
||||
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
|
||||
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_CURRENT_TEMPERATURE_TOPIC):
|
||||
mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_FAN_MODE_LIST,
|
||||
default=[STATE_AUTO, SPEED_LOW,
|
||||
SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list,
|
||||
vol.Optional(CONF_SWING_MODE_LIST,
|
||||
default=[STATE_ON, STATE_OFF]): cv.ensure_list,
|
||||
vol.Optional(CONF_MODE_LIST,
|
||||
default=[STATE_AUTO, STATE_OFF, STATE_COOL, STATE_HEAT,
|
||||
STATE_DRY, STATE_FAN_ONLY]): cv.ensure_list,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_INITIAL, default=21): cv.positive_int,
|
||||
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT climate devices."""
|
||||
async_add_devices([
|
||||
MqttClimate(
|
||||
hass,
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_POWER_COMMAND_TOPIC,
|
||||
CONF_MODE_COMMAND_TOPIC,
|
||||
CONF_TEMPERATURE_COMMAND_TOPIC,
|
||||
CONF_FAN_MODE_COMMAND_TOPIC,
|
||||
CONF_SWING_MODE_COMMAND_TOPIC,
|
||||
CONF_AWAY_MODE_COMMAND_TOPIC,
|
||||
CONF_HOLD_COMMAND_TOPIC,
|
||||
CONF_AUX_COMMAND_TOPIC,
|
||||
CONF_POWER_STATE_TOPIC,
|
||||
CONF_MODE_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_STATE_TOPIC,
|
||||
CONF_FAN_MODE_STATE_TOPIC,
|
||||
CONF_SWING_MODE_STATE_TOPIC,
|
||||
CONF_AWAY_MODE_STATE_TOPIC,
|
||||
CONF_HOLD_STATE_TOPIC,
|
||||
CONF_AUX_STATE_TOPIC,
|
||||
CONF_CURRENT_TEMPERATURE_TOPIC
|
||||
)
|
||||
},
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
config.get(CONF_MODE_LIST),
|
||||
config.get(CONF_FAN_MODE_LIST),
|
||||
config.get(CONF_SWING_MODE_LIST),
|
||||
config.get(CONF_INITIAL),
|
||||
False, None, SPEED_LOW,
|
||||
STATE_OFF, STATE_OFF, False,
|
||||
config.get(CONF_SEND_IF_OFF),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF))
|
||||
])
|
||||
|
||||
|
||||
class MqttClimate(ClimateDevice):
|
||||
"""Representation of a demo climate device."""
|
||||
|
||||
def __init__(self, hass, name, topic, qos, retain, mode_list,
|
||||
fan_mode_list, swing_mode_list, target_temperature, away,
|
||||
hold, current_fan_mode, current_swing_mode,
|
||||
current_operation, aux, send_if_off, payload_on,
|
||||
payload_off):
|
||||
"""Initialize the climate device."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._target_temperature = target_temperature
|
||||
self._unit_of_measurement = hass.config.units.temperature_unit
|
||||
self._away = away
|
||||
self._hold = hold
|
||||
self._current_temperature = None
|
||||
self._current_fan_mode = current_fan_mode
|
||||
self._current_operation = current_operation
|
||||
self._aux = aux
|
||||
self._current_swing_mode = current_swing_mode
|
||||
self._fan_list = fan_mode_list
|
||||
self._operation_list = mode_list
|
||||
self._swing_list = swing_mode_list
|
||||
self._target_temperature_step = 1
|
||||
self._send_if_off = send_if_off
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Handle being added to home assistant."""
|
||||
@callback
|
||||
def handle_current_temp_received(topic, payload, qos):
|
||||
"""Handle current temperature coming via MQTT."""
|
||||
try:
|
||||
self._current_temperature = float(payload)
|
||||
self.async_schedule_update_ha_state()
|
||||
except ValueError:
|
||||
_LOGGER.error("Could not parse temperature from %s", payload)
|
||||
|
||||
if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_CURRENT_TEMPERATURE_TOPIC],
|
||||
handle_current_temp_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_mode_received(topic, payload, qos):
|
||||
"""Handle receiving mode via MQTT."""
|
||||
if payload not in self._operation_list:
|
||||
_LOGGER.error("Invalid mode: %s", payload)
|
||||
else:
|
||||
self._current_operation = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_MODE_STATE_TOPIC],
|
||||
handle_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_temperature_received(topic, payload, qos):
|
||||
"""Handle target temperature coming via MQTT."""
|
||||
try:
|
||||
self._target_temperature = float(payload)
|
||||
self.async_schedule_update_ha_state()
|
||||
except ValueError:
|
||||
_LOGGER.error("Could not parse temperature from %s", payload)
|
||||
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_TEMPERATURE_STATE_TOPIC],
|
||||
handle_temperature_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_fan_mode_received(topic, payload, qos):
|
||||
"""Handle receiving fan mode via MQTT."""
|
||||
if payload not in self._fan_list:
|
||||
_LOGGER.error("Invalid fan mode: %s", payload)
|
||||
else:
|
||||
self._current_fan_mode = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_FAN_MODE_STATE_TOPIC],
|
||||
handle_fan_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_swing_mode_received(topic, payload, qos):
|
||||
"""Handle receiving swing mode via MQTT."""
|
||||
if payload not in self._swing_list:
|
||||
_LOGGER.error("Invalid swing mode: %s", payload)
|
||||
else:
|
||||
self._current_swing_mode = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_SWING_MODE_STATE_TOPIC],
|
||||
handle_swing_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_away_mode_received(topic, payload, qos):
|
||||
"""Handle receiving away mode via MQTT."""
|
||||
if payload == self._payload_on:
|
||||
self._away = True
|
||||
elif payload == self._payload_off:
|
||||
self._away = False
|
||||
else:
|
||||
_LOGGER.error("Invalid away mode: %s", payload)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_AWAY_MODE_STATE_TOPIC],
|
||||
handle_away_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_aux_mode_received(topic, payload, qos):
|
||||
"""Handle receiving aux mode via MQTT."""
|
||||
if payload == self._payload_on:
|
||||
self._aux = True
|
||||
elif payload == self._payload_off:
|
||||
self._aux = False
|
||||
else:
|
||||
_LOGGER.error("Invalid aux mode: %s", payload)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_AUX_STATE_TOPIC],
|
||||
handle_aux_mode_received, self._qos)
|
||||
|
||||
@callback
|
||||
def handle_hold_mode_received(topic, payload, qos):
|
||||
"""Handle receiving hold mode via MQTT."""
|
||||
self._hold = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_HOLD_STATE_TOPIC] is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_HOLD_STATE_TOPIC],
|
||||
handle_hold_mode_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the climate device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self._target_temperature_step
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if away mode is on."""
|
||||
return self._away
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return hold mode setting."""
|
||||
return self._hold
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._aux
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
if kwargs.get(ATTR_OPERATION_MODE) is not None:
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
yield from self.async_set_operation_mode(operation_mode)
|
||||
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
|
||||
# optimistic mode
|
||||
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_TEMPERATURE_COMMAND_TOPIC],
|
||||
kwargs.get(ATTR_TEMPERATURE), self._qos, self._retain)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new swing mode."""
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_SWING_MODE_COMMAND_TOPIC],
|
||||
swing_mode, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
|
||||
self._current_swing_mode = swing_mode
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_fan_mode(self, fan):
|
||||
"""Set new target temperature."""
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_FAN_MODE_COMMAND_TOPIC],
|
||||
fan, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
|
||||
self._current_fan_mode = fan
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_operation_mode(self, operation_mode) -> None:
|
||||
"""Set new operation mode."""
|
||||
if self._topic[CONF_POWER_COMMAND_TOPIC] is not None:
|
||||
if (self._current_operation == STATE_OFF and
|
||||
operation_mode != STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
elif (self._current_operation != STATE_OFF and
|
||||
operation_mode == STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_MODE_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_MODE_COMMAND_TOPIC],
|
||||
operation_mode, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is None:
|
||||
self._current_operation = operation_mode
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the swing setting."""
|
||||
return self._current_swing_mode
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return self._swing_list
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
|
||||
self._away = True
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
|
||||
self._away = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_hold_mode(self, hold):
|
||||
"""Update hold mode on."""
|
||||
if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_HOLD_COMMAND_TOPIC],
|
||||
hold, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_HOLD_STATE_TOPIC] is None:
|
||||
self._hold = hold
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is None:
|
||||
self._aux = True
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is None:
|
||||
self._aux = False
|
||||
self.async_schedule_update_ha_state()
|
||||
@@ -1,132 +1,102 @@
|
||||
# Describes the format for available climate services
|
||||
|
||||
set_aux_heat:
|
||||
description: Turn auxillary heater on/off for climate device
|
||||
|
||||
description: Turn auxiliary heater on/off for climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
aux_heat:
|
||||
description: New value of axillary heater
|
||||
description: New value of axillary heater.
|
||||
example: true
|
||||
|
||||
set_away_mode:
|
||||
description: Turn away mode on/off for climate device
|
||||
|
||||
description: Turn away mode on/off for climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
away_mode:
|
||||
description: New value of away mode
|
||||
description: New value of away mode.
|
||||
example: true
|
||||
|
||||
set_hold_mode:
|
||||
description: Turn hold mode for climate device
|
||||
|
||||
description: Turn hold mode for climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
hold_mode:
|
||||
description: New value of hold mode
|
||||
example: 'away'
|
||||
|
||||
set_temperature:
|
||||
description: Set target temperature of climate device
|
||||
|
||||
description: Set target temperature of climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
temperature:
|
||||
description: New target temperature for hvac
|
||||
description: New target temperature for HVAC.
|
||||
example: 25
|
||||
|
||||
target_temp_high:
|
||||
description: New target high tempereature for hvac
|
||||
description: New target high tempereature for HVAC.
|
||||
example: 26
|
||||
|
||||
target_temp_low:
|
||||
description: New target low temperature for hvac
|
||||
description: New target low temperature for HVAC.
|
||||
example: 20
|
||||
|
||||
operation_mode:
|
||||
description: Operation mode to set temperature to. This defaults to current_operation mode if not set, or set incorrectly.
|
||||
example: 'Heat'
|
||||
|
||||
set_humidity:
|
||||
description: Set target humidity of climate device
|
||||
|
||||
description: Set target humidity of climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
humidity:
|
||||
description: New target humidity for climate device
|
||||
description: New target humidity for climate device.
|
||||
example: 60
|
||||
|
||||
set_fan_mode:
|
||||
description: Set fan operation for climate device
|
||||
|
||||
description: Set fan operation for climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.nest'
|
||||
|
||||
fan_mode:
|
||||
description: New value of fan mode
|
||||
description: New value of fan mode.
|
||||
example: On Low
|
||||
|
||||
set_operation_mode:
|
||||
description: Set operation mode for climate device
|
||||
|
||||
description: Set operation mode for climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.nest'
|
||||
|
||||
operation_mode:
|
||||
description: New value of operation mode
|
||||
description: New value of operation mode.
|
||||
example: Heat
|
||||
|
||||
|
||||
set_swing_mode:
|
||||
description: Set swing operation for climate device
|
||||
|
||||
description: Set swing operation for climate device.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: '.nest'
|
||||
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.nest'
|
||||
swing_mode:
|
||||
description: New value of swing mode
|
||||
example: 1
|
||||
|
||||
description: New value of swing mode.
|
||||
example: 1
|
||||
ecobee_set_fan_min_on_time:
|
||||
description: Set the minimum fan on time
|
||||
|
||||
description: Set the minimum fan on time.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
fan_min_on_time:
|
||||
description: New value of fan min on time
|
||||
description: New value of fan min on time.
|
||||
example: 5
|
||||
|
||||
ecobee_resume_program:
|
||||
description: Resume the programmed schedule
|
||||
|
||||
description: Resume the programmed schedule.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
resume_all:
|
||||
description: Resume all events and return to the scheduled program. This default to false which removes only the top event.
|
||||
example: true
|
||||
|
||||
@@ -35,7 +35,6 @@ class TeslaThermostat(TeslaDevice, ClimateDevice):
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._name = self.tesla_device.name
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
90
homeassistant/components/climate/toon.py
Normal file
90
homeassistant/components/climate/toon.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Toon van Eneco Thermostat Support.
|
||||
|
||||
This provides a component for the rebranded Quby thermostat as provided by
|
||||
Eneco.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.toon/
|
||||
"""
|
||||
import homeassistant.components.toon as toon_main
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ATTR_TEMPERATURE, STATE_PERFORMANCE, STATE_HEAT, STATE_ECO,
|
||||
STATE_COOL)
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Toon thermostat."""
|
||||
add_devices([ThermostatDevice(hass)], True)
|
||||
|
||||
|
||||
class ThermostatDevice(ClimateDevice):
|
||||
"""Interface class for the toon module and HA."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the device."""
|
||||
self._name = 'Toon van Eneco'
|
||||
self.hass = hass
|
||||
self.thermos = hass.data[toon_main.TOON_HANDLE]
|
||||
|
||||
self._state = None
|
||||
self._temperature = None
|
||||
self._setpoint = None
|
||||
self._operation_list = [
|
||||
STATE_PERFORMANCE,
|
||||
STATE_HEAT,
|
||||
STATE_ECO,
|
||||
STATE_COOL,
|
||||
]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of this Thermostat."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""The unit of measurement used by the platform."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation i.e. comfort, home, away."""
|
||||
state = self.thermos.get_data('state')
|
||||
return state
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.thermos.get_data('temp')
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.thermos.get_data('setpoint')
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Change the setpoint of the thermostat."""
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.thermos.set_temp(temp)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new operation mode as toonlib requires it."""
|
||||
toonlib_values = {
|
||||
STATE_PERFORMANCE: 'Comfort',
|
||||
STATE_HEAT: 'Home',
|
||||
STATE_ECO: 'Away',
|
||||
STATE_COOL: 'Sleep',
|
||||
}
|
||||
|
||||
self.thermos.set_state(toonlib_values[operation_mode])
|
||||
|
||||
def update(self):
|
||||
"""Update local state."""
|
||||
self.thermos.update()
|
||||
@@ -1,30 +1,45 @@
|
||||
"""
|
||||
Support for Wink thermostats.
|
||||
Support for Wink thermostats, Air Conditioners, and Water Heaters.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.wink/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_CURRENT_HUMIDITY)
|
||||
ATTR_TEMPERATURE, STATE_FAN_ONLY,
|
||||
ATTR_CURRENT_HUMIDITY, STATE_ECO, STATE_ELECTRIC,
|
||||
STATE_PERFORMANCE, STATE_HIGH_DEMAND,
|
||||
STATE_HEAT_PUMP, STATE_GAS)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, STATE_ON,
|
||||
STATE_OFF, STATE_UNKNOWN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
STATE_AUX = 'aux'
|
||||
STATE_ECO = 'eco'
|
||||
STATE_FAN = 'fan'
|
||||
SPEED_LOW = 'low'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
|
||||
HA_STATE_TO_WINK = {STATE_AUTO: 'auto',
|
||||
STATE_ECO: 'eco',
|
||||
STATE_FAN_ONLY: 'fan_only',
|
||||
STATE_HEAT: 'heat_only',
|
||||
STATE_COOL: 'cool_only',
|
||||
STATE_PERFORMANCE: 'performance',
|
||||
STATE_HIGH_DEMAND: 'high_demand',
|
||||
STATE_HEAT_PUMP: 'heat_pump',
|
||||
STATE_ELECTRIC: 'electric_only',
|
||||
STATE_GAS: 'gas',
|
||||
STATE_OFF: 'off'}
|
||||
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
|
||||
|
||||
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
ATTR_SMART_TEMPERATURE = "smart_temperature"
|
||||
ATTR_ECO_TARGET = "eco_target"
|
||||
@@ -32,28 +47,26 @@ ATTR_OCCUPIED = "occupied"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Wink thermostat."""
|
||||
"""Set up the Wink climate devices."""
|
||||
import pywink
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
for climate in pywink.get_thermostats():
|
||||
_id = climate.object_id() + climate.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkThermostat(climate, hass, temp_unit)])
|
||||
add_devices([WinkThermostat(climate, hass)])
|
||||
for climate in pywink.get_air_conditioners():
|
||||
_id = climate.object_id() + climate.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkAC(climate, hass, temp_unit)])
|
||||
add_devices([WinkAC(climate, hass)])
|
||||
for water_heater in pywink.get_water_heaters():
|
||||
_id = water_heater.object_id() + water_heater.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkWaterHeater(water_heater, hass)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink thermostat."""
|
||||
|
||||
def __init__(self, wink, hass, temp_unit):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink, hass)
|
||||
self._config_temp_unit = temp_unit
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
@@ -139,18 +152,12 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
elif self.wink.current_hvac_mode() == 'cool_only':
|
||||
current_op = STATE_COOL
|
||||
elif self.wink.current_hvac_mode() == 'heat_only':
|
||||
current_op = STATE_HEAT
|
||||
elif self.wink.current_hvac_mode() == 'aux':
|
||||
current_op = STATE_HEAT
|
||||
elif self.wink.current_hvac_mode() == 'auto':
|
||||
current_op = STATE_AUTO
|
||||
elif self.wink.current_hvac_mode() == 'eco':
|
||||
current_op = STATE_ECO
|
||||
else:
|
||||
current_op = STATE_UNKNOWN
|
||||
current_op = WINK_STATE_TO_HA.get(self.wink.current_hvac_mode())
|
||||
if current_op == 'aux':
|
||||
return STATE_HEAT
|
||||
if current_op is None:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
|
||||
@property
|
||||
@@ -199,11 +206,12 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if aux heater."""
|
||||
if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on():
|
||||
if 'aux' not in self.wink.hvac_modes():
|
||||
return None
|
||||
|
||||
if self.wink.current_hvac_mode() == 'aux':
|
||||
return True
|
||||
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
|
||||
return False
|
||||
return None
|
||||
return False
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -223,32 +231,27 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.wink.set_operation_mode('heat_only')
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.wink.set_operation_mode('cool_only')
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.wink.set_operation_mode('auto')
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.wink.set_operation_mode('off')
|
||||
elif operation_mode == STATE_AUX:
|
||||
self.wink.set_operation_mode('aux')
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.wink.set_operation_mode('eco')
|
||||
op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode)
|
||||
# The only way to disable aux heat is with the toggle
|
||||
if self.is_aux_heat_on and op_mode_to_set == STATE_HEAT:
|
||||
return
|
||||
self.wink.set_operation_mode(op_mode_to_set)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
op_list = ['off']
|
||||
modes = self.wink.hvac_modes()
|
||||
if 'cool_only' in modes:
|
||||
op_list.append(STATE_COOL)
|
||||
if 'heat_only' in modes or 'aux' in modes:
|
||||
op_list.append(STATE_HEAT)
|
||||
if 'auto' in modes:
|
||||
op_list.append(STATE_AUTO)
|
||||
if 'eco' in modes:
|
||||
op_list.append(STATE_ECO)
|
||||
for mode in modes:
|
||||
if mode == 'aux':
|
||||
continue
|
||||
ha_mode = WINK_STATE_TO_HA.get(mode)
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invaid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
@@ -281,12 +284,12 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
self.wink.set_fan_mode(fan.lower())
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
self.set_operation_mode(STATE_AUX)
|
||||
"""Turn auxiliary heater on."""
|
||||
self.wink.set_operation_mode('aux')
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
self.set_operation_mode(STATE_AUTO)
|
||||
"""Turn auxiliary heater off."""
|
||||
self.set_operation_mode(STATE_HEAT)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
@@ -344,11 +347,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
class WinkAC(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink air conditioner."""
|
||||
|
||||
def __init__(self, wink, hass, temp_unit):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink, hass)
|
||||
self._config_temp_unit = temp_unit
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
@@ -382,14 +380,10 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
elif self.wink.current_mode() == 'cool_only':
|
||||
current_op = STATE_COOL
|
||||
elif self.wink.current_mode() == 'auto_eco':
|
||||
current_op = STATE_ECO
|
||||
elif self.wink.current_mode() == 'fan_only':
|
||||
current_op = STATE_FAN
|
||||
else:
|
||||
current_op = STATE_UNKNOWN
|
||||
current_op = WINK_STATE_TO_HA.get(self.wink.current_hvac_mode())
|
||||
if current_op is None:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
|
||||
@property
|
||||
@@ -397,12 +391,14 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
"""List of available operation modes."""
|
||||
op_list = ['off']
|
||||
modes = self.wink.modes()
|
||||
if 'cool_only' in modes:
|
||||
op_list.append(STATE_COOL)
|
||||
if 'auto_eco' in modes:
|
||||
op_list.append(STATE_ECO)
|
||||
if 'fan_only' in modes:
|
||||
op_list.append(STATE_FAN)
|
||||
for mode in modes:
|
||||
ha_mode = WINK_STATE_TO_HA.get(mode)
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invaid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
@@ -412,30 +408,16 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_COOL:
|
||||
self.wink.set_operation_mode('cool_only')
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.wink.set_operation_mode('auto_eco')
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.wink.set_operation_mode('off')
|
||||
elif operation_mode == STATE_FAN:
|
||||
self.wink.set_operation_mode('fan_only')
|
||||
op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode)
|
||||
if op_mode_to_set == 'eco':
|
||||
op_mode_to_set = 'auto_eco'
|
||||
self.wink.set_operation_mode(op_mode_to_set)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.wink.current_max_set_point()
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Only supports cool."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Only supports cool."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the current fan mode."""
|
||||
@@ -453,12 +435,97 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
"""Return a list of available fan modes."""
|
||||
return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
def set_fan_mode(self, mode):
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set fan speed."""
|
||||
if mode == SPEED_LOW:
|
||||
if fan == SPEED_LOW:
|
||||
speed = 0.4
|
||||
elif mode == SPEED_MEDIUM:
|
||||
elif fan == SPEED_MEDIUM:
|
||||
speed = 0.8
|
||||
elif mode == SPEED_HIGH:
|
||||
elif fan == SPEED_HIGH:
|
||||
speed = 1.0
|
||||
self.wink.set_ac_fan_speed(speed)
|
||||
|
||||
|
||||
class WinkWaterHeater(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink water heater."""
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
# The Wink API always returns temp in Celsius
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {}
|
||||
data["vacation_mode"] = self.wink.vacation_mode_enabled()
|
||||
data["rheem_type"] = self.wink.rheem_type()
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""
|
||||
Return current operation one of the following.
|
||||
|
||||
["eco", "performance", "heat_pump",
|
||||
"high_demand", "electric_only", "gas]
|
||||
"""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
else:
|
||||
current_op = WINK_STATE_TO_HA.get(self.wink.current_mode())
|
||||
if current_op is None:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
op_list = ['off']
|
||||
modes = self.wink.modes()
|
||||
for mode in modes:
|
||||
if mode == 'aux':
|
||||
continue
|
||||
ha_mode = WINK_STATE_TO_HA.get(mode)
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invaid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.wink.set_temperature(target_temp)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode)
|
||||
self.wink.set_operation_mode(op_mode_to_set)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.wink.current_set_point()
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.wink.set_vacation_mode(True)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.wink.set_vacation_mode(False)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self.wink.min_set_point()
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self.wink.max_set_point()
|
||||
|
||||
@@ -1,47 +1,148 @@
|
||||
"""Component to integrate the Home Assistant cloud."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from . import http_api, auth_api
|
||||
from .const import DOMAIN
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
|
||||
|
||||
from . import http_api, iot
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
|
||||
REQUIREMENTS = ['warrant==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
|
||||
CONF_RELAYER = 'relayer'
|
||||
CONF_USER_POOL_ID = 'user_pool_id'
|
||||
|
||||
REQUIREMENTS = ['warrant==0.2.0']
|
||||
DEPENDENCIES = ['http']
|
||||
CONF_MODE = 'mode'
|
||||
MODE_DEV = 'development'
|
||||
MODE_STAGING = 'staging'
|
||||
MODE_PRODUCTION = 'production'
|
||||
DEFAULT_MODE = MODE_DEV
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
|
||||
vol.In([MODE_DEV, MODE_STAGING, MODE_PRODUCTION]),
|
||||
vol.In([MODE_DEV] + list(SERVERS)),
|
||||
# Change to optional when we include real servers
|
||||
vol.Required(CONF_COGNITO_CLIENT_ID): str,
|
||||
vol.Required(CONF_USER_POOL_ID): str,
|
||||
vol.Required(CONF_REGION): str,
|
||||
vol.Required(CONF_RELAYER): str,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Initialize the Home Assistant cloud."""
|
||||
mode = MODE_PRODUCTION
|
||||
|
||||
if DOMAIN in config:
|
||||
mode = config[DOMAIN].get(CONF_MODE)
|
||||
kwargs = config[DOMAIN]
|
||||
else:
|
||||
kwargs = {CONF_MODE: DEFAULT_MODE}
|
||||
|
||||
if mode != 'development':
|
||||
_LOGGER.error('Only development mode is currently allowed.')
|
||||
return False
|
||||
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
||||
|
||||
data = hass.data[DOMAIN] = {
|
||||
'mode': mode
|
||||
}
|
||||
@asyncio.coroutine
|
||||
def init_cloud(event):
|
||||
"""Initialize connection."""
|
||||
yield from cloud.initialize()
|
||||
|
||||
data['auth'] = yield from hass.async_add_job(auth_api.load_auth, hass)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, init_cloud)
|
||||
|
||||
yield from http_api.async_setup(hass)
|
||||
return True
|
||||
|
||||
|
||||
class Cloud:
|
||||
"""Store the configuration of the cloud connection."""
|
||||
|
||||
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
|
||||
region=None, relayer=None):
|
||||
"""Create an instance of Cloud."""
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
self.email = None
|
||||
self.id_token = None
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
self.iot = iot.CloudIoT(self)
|
||||
|
||||
if mode == MODE_DEV:
|
||||
self.cognito_client_id = cognito_client_id
|
||||
self.user_pool_id = user_pool_id
|
||||
self.region = region
|
||||
self.relayer = relayer
|
||||
|
||||
else:
|
||||
info = SERVERS[mode]
|
||||
|
||||
self.cognito_client_id = info['cognito_client_id']
|
||||
self.user_pool_id = info['user_pool_id']
|
||||
self.region = info['region']
|
||||
self.relayer = info['relayer']
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
"""Get if cloud is logged in."""
|
||||
return self.email is not None
|
||||
|
||||
@property
|
||||
def user_info_path(self):
|
||||
"""Get path to the stored auth."""
|
||||
return self.path('{}_auth.json'.format(self.mode))
|
||||
|
||||
@asyncio.coroutine
|
||||
def initialize(self):
|
||||
"""Initialize and load cloud info."""
|
||||
def load_config():
|
||||
"""Load the configuration."""
|
||||
# Ensure config dir exists
|
||||
path = self.hass.config.path(CONFIG_DIR)
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
|
||||
user_info = self.user_info_path
|
||||
if os.path.isfile(user_info):
|
||||
with open(user_info, 'rt') as file:
|
||||
info = json.loads(file.read())
|
||||
self.email = info['email']
|
||||
self.id_token = info['id_token']
|
||||
self.access_token = info['access_token']
|
||||
self.refresh_token = info['refresh_token']
|
||||
|
||||
yield from self.hass.async_add_job(load_config)
|
||||
|
||||
if self.email is not None:
|
||||
yield from self.iot.connect()
|
||||
|
||||
def path(self, *parts):
|
||||
"""Get config path inside cloud dir."""
|
||||
return self.hass.config.path(CONFIG_DIR, *parts)
|
||||
|
||||
@asyncio.coroutine
|
||||
def logout(self):
|
||||
"""Close connection and remove all credentials."""
|
||||
yield from self.iot.disconnect()
|
||||
|
||||
self.email = None
|
||||
self.id_token = None
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
|
||||
yield from self.hass.async_add_job(
|
||||
lambda: os.remove(self.user_info_path))
|
||||
|
||||
def write_user_info(self):
|
||||
"""Write user info to a file."""
|
||||
with open(self.user_info_path, 'wt') as file:
|
||||
file.write(json.dumps({
|
||||
'email': self.email,
|
||||
'id_token': self.id_token,
|
||||
'access_token': self.access_token,
|
||||
'refresh_token': self.refresh_token,
|
||||
}, indent=4))
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"""Package to offer tools to authenticate with the cloud."""
|
||||
import json
|
||||
"""Package to communicate with the authentication API."""
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .const import AUTH_FILE, SERVERS
|
||||
from .util import get_mode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,210 +58,120 @@ def _map_aws_exception(err):
|
||||
return ex(err.response['Error']['Message'])
|
||||
|
||||
|
||||
def load_auth(hass):
|
||||
"""Load authentication from disk and verify it."""
|
||||
info = _read_info(hass)
|
||||
|
||||
if info is None:
|
||||
return Auth(hass)
|
||||
|
||||
auth = Auth(hass, _cognito(
|
||||
hass,
|
||||
id_token=info['id_token'],
|
||||
access_token=info['access_token'],
|
||||
refresh_token=info['refresh_token'],
|
||||
))
|
||||
|
||||
if auth.validate_auth():
|
||||
return auth
|
||||
|
||||
return Auth(hass)
|
||||
def _generate_username(email):
|
||||
"""Generate a username from an email address."""
|
||||
return hashlib.sha512(email.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def register(hass, email, password):
|
||||
def register(cloud, email, password):
|
||||
"""Register a new account."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
cognito = _cognito(cloud)
|
||||
try:
|
||||
cognito.register(email, password)
|
||||
cognito.register(_generate_username(email), password, email=email)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def confirm_register(hass, confirmation_code, email):
|
||||
def confirm_register(cloud, confirmation_code, email):
|
||||
"""Confirm confirmation code after registration."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
cognito = _cognito(cloud)
|
||||
try:
|
||||
cognito.confirm_sign_up(confirmation_code, email)
|
||||
cognito.confirm_sign_up(confirmation_code, _generate_username(email))
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def forgot_password(hass, email):
|
||||
def forgot_password(cloud, email):
|
||||
"""Initiate forgotten password flow."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
try:
|
||||
cognito.initiate_forgot_password()
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def confirm_forgot_password(hass, confirmation_code, email, new_password):
|
||||
def confirm_forgot_password(cloud, confirmation_code, email, new_password):
|
||||
"""Confirm forgotten password code and change password."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
try:
|
||||
cognito.confirm_forgot_password(confirmation_code, new_password)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
class Auth(object):
|
||||
"""Class that holds Cloud authentication."""
|
||||
|
||||
def __init__(self, hass, cognito=None):
|
||||
"""Initialize Hass cloud info object."""
|
||||
self.hass = hass
|
||||
self.cognito = cognito
|
||||
self.account = None
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
"""Return if user is logged in."""
|
||||
return self.account is not None
|
||||
|
||||
def validate_auth(self):
|
||||
"""Validate that the contained auth is valid."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
try:
|
||||
self._refresh_account_info()
|
||||
except ClientError as err:
|
||||
if err.response['Error']['Code'] != 'NotAuthorizedException':
|
||||
_LOGGER.error('Unexpected error verifying auth: %s', err)
|
||||
return False
|
||||
|
||||
try:
|
||||
self.renew_access_token()
|
||||
self._refresh_account_info()
|
||||
except ClientError:
|
||||
_LOGGER.error('Unable to refresh auth token: %s', err)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def login(self, username, password):
|
||||
"""Login using a username and password."""
|
||||
from botocore.exceptions import ClientError
|
||||
from warrant.exceptions import ForceChangePasswordException
|
||||
|
||||
cognito = _cognito(self.hass, username=username)
|
||||
|
||||
try:
|
||||
cognito.authenticate(password=password)
|
||||
self.cognito = cognito
|
||||
self._refresh_account_info()
|
||||
_write_info(self.hass, self)
|
||||
|
||||
except ForceChangePasswordException as err:
|
||||
raise PasswordChangeRequired
|
||||
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
def _refresh_account_info(self):
|
||||
"""Refresh the account info.
|
||||
|
||||
Raises boto3 exceptions.
|
||||
"""
|
||||
self.account = self.cognito.get_user()
|
||||
|
||||
def renew_access_token(self):
|
||||
"""Refresh token."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
try:
|
||||
self.cognito.renew_access_token()
|
||||
_write_info(self.hass, self)
|
||||
return True
|
||||
except ClientError as err:
|
||||
_LOGGER.error('Error refreshing token: %s', err)
|
||||
return False
|
||||
|
||||
def logout(self):
|
||||
"""Invalidate token."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
try:
|
||||
self.cognito.logout()
|
||||
self.account = None
|
||||
_write_info(self.hass, self)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
def login(cloud, email, password):
|
||||
"""Log user in and fetch certificate."""
|
||||
cognito = _authenticate(cloud, email, password)
|
||||
cloud.id_token = cognito.id_token
|
||||
cloud.access_token = cognito.access_token
|
||||
cloud.refresh_token = cognito.refresh_token
|
||||
cloud.email = email
|
||||
cloud.write_user_info()
|
||||
|
||||
|
||||
def _read_info(hass):
|
||||
"""Read auth file."""
|
||||
path = hass.config.path(AUTH_FILE)
|
||||
def check_token(cloud):
|
||||
"""Check that the token is valid and verify if needed."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
cognito = _cognito(
|
||||
cloud,
|
||||
access_token=cloud.access_token,
|
||||
refresh_token=cloud.refresh_token)
|
||||
|
||||
with open(path) as file:
|
||||
return json.load(file).get(get_mode(hass))
|
||||
try:
|
||||
if cognito.check_token():
|
||||
cloud.id_token = cognito.id_token
|
||||
cloud.access_token = cognito.access_token
|
||||
cloud.write_user_info()
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def _write_info(hass, auth):
|
||||
"""Write auth info for specified mode.
|
||||
def _authenticate(cloud, email, password):
|
||||
"""Log in and return an authenticated Cognito instance."""
|
||||
from botocore.exceptions import ClientError
|
||||
from warrant.exceptions import ForceChangePasswordException
|
||||
|
||||
Pass in None for data to remove authentication for that mode.
|
||||
"""
|
||||
path = hass.config.path(AUTH_FILE)
|
||||
mode = get_mode(hass)
|
||||
assert not cloud.is_logged_in, 'Cannot login if already logged in.'
|
||||
|
||||
if os.path.isfile(path):
|
||||
with open(path) as file:
|
||||
content = json.load(file)
|
||||
else:
|
||||
content = {}
|
||||
cognito = _cognito(cloud, username=email)
|
||||
|
||||
if auth.is_logged_in:
|
||||
content[mode] = {
|
||||
'id_token': auth.cognito.id_token,
|
||||
'access_token': auth.cognito.access_token,
|
||||
'refresh_token': auth.cognito.refresh_token,
|
||||
}
|
||||
else:
|
||||
content.pop(mode, None)
|
||||
try:
|
||||
cognito.authenticate(password=password)
|
||||
return cognito
|
||||
|
||||
with open(path, 'wt') as file:
|
||||
file.write(json.dumps(content, indent=4, sort_keys=True))
|
||||
except ForceChangePasswordException as err:
|
||||
raise PasswordChangeRequired
|
||||
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def _cognito(hass, **kwargs):
|
||||
def _cognito(cloud, **kwargs):
|
||||
"""Get the client credentials."""
|
||||
import botocore
|
||||
import boto3
|
||||
from warrant import Cognito
|
||||
|
||||
mode = get_mode(hass)
|
||||
|
||||
info = SERVERS.get(mode)
|
||||
|
||||
if info is None:
|
||||
raise ValueError('Mode {} is not supported.'.format(mode))
|
||||
|
||||
cognito = Cognito(
|
||||
user_pool_id=info['identity_pool_id'],
|
||||
client_id=info['client_id'],
|
||||
user_pool_region=info['region'],
|
||||
access_key=info['access_key_id'],
|
||||
secret_key=info['secret_access_key'],
|
||||
user_pool_id=cloud.user_pool_id,
|
||||
client_id=cloud.cognito_client_id,
|
||||
user_pool_region=cloud.region,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
cognito.client = boto3.client(
|
||||
'cognito-idp',
|
||||
region_name=cloud.region,
|
||||
config=botocore.config.Config(
|
||||
signature_version=botocore.UNSIGNED
|
||||
)
|
||||
)
|
||||
return cognito
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""Constants for the cloud component."""
|
||||
DOMAIN = 'cloud'
|
||||
CONFIG_DIR = '.cloud'
|
||||
REQUEST_TIMEOUT = 10
|
||||
AUTH_FILE = '.cloud'
|
||||
|
||||
SERVERS = {
|
||||
'development': {
|
||||
'client_id': '3k755iqfcgv8t12o4pl662mnos',
|
||||
'identity_pool_id': 'us-west-2_vDOfweDJo',
|
||||
'region': 'us-west-2',
|
||||
'access_key_id': 'AKIAJGRK7MILPRJTT2ZQ',
|
||||
'secret_access_key': 'lscdYBApxrLWL0HKuVqVXWv3ou8ZVXgG7rZBu/Sz'
|
||||
}
|
||||
# Example entry:
|
||||
# 'production': {
|
||||
# 'cognito_client_id': '',
|
||||
# 'user_pool_id': '',
|
||||
# 'region': '',
|
||||
# 'relayer': ''
|
||||
# }
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.components.http import (
|
||||
HomeAssistantView, RequestDataValidator)
|
||||
|
||||
from . import auth_api
|
||||
from .const import REQUEST_TIMEOUT
|
||||
from .const import DOMAIN, REQUEST_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -74,13 +74,14 @@ class CloudLoginView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle login request."""
|
||||
hass = request.app['hass']
|
||||
auth = hass.data['cloud']['auth']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(auth.login, data['email'],
|
||||
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
|
||||
data['password'])
|
||||
hass.async_add_job(cloud.iot.connect)
|
||||
|
||||
return self.json(_auth_data(auth))
|
||||
return self.json(_account_data(cloud))
|
||||
|
||||
|
||||
class CloudLogoutView(HomeAssistantView):
|
||||
@@ -94,10 +95,10 @@ class CloudLogoutView(HomeAssistantView):
|
||||
def post(self, request):
|
||||
"""Handle logout request."""
|
||||
hass = request.app['hass']
|
||||
auth = hass.data['cloud']['auth']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(auth.logout)
|
||||
yield from cloud.logout()
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
@@ -112,12 +113,12 @@ class CloudAccountView(HomeAssistantView):
|
||||
def get(self, request):
|
||||
"""Get account info."""
|
||||
hass = request.app['hass']
|
||||
auth = hass.data['cloud']['auth']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
if not auth.is_logged_in:
|
||||
if not cloud.is_logged_in:
|
||||
return self.json_message('Not logged in', 400)
|
||||
|
||||
return self.json(_auth_data(auth))
|
||||
return self.json(_account_data(cloud))
|
||||
|
||||
|
||||
class CloudRegisterView(HomeAssistantView):
|
||||
@@ -135,10 +136,11 @@ class CloudRegisterView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle registration request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.register, hass, data['email'], data['password'])
|
||||
auth_api.register, cloud, data['email'], data['password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
@@ -158,10 +160,11 @@ class CloudConfirmRegisterView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle registration confirmation request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_register, hass, data['confirmation_code'],
|
||||
auth_api.confirm_register, cloud, data['confirmation_code'],
|
||||
data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
@@ -181,10 +184,11 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.forgot_password, hass, data['email'])
|
||||
auth_api.forgot_password, cloud, data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
@@ -205,18 +209,19 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password confirm request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_forgot_password, hass,
|
||||
auth_api.confirm_forgot_password, cloud,
|
||||
data['confirmation_code'], data['email'],
|
||||
data['new_password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
def _auth_data(auth):
|
||||
def _account_data(cloud):
|
||||
"""Generate the auth data JSON response."""
|
||||
return {
|
||||
'email': auth.account.email
|
||||
'email': cloud.email
|
||||
}
|
||||
|
||||
194
homeassistant/components/cloud/iot.py
Normal file
194
homeassistant/components/cloud/iot.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""Module to handle messages from Home Assistant cloud."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiohttp import hdrs, client_exceptions, WSMsgType
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.alexa import smart_home
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from . import auth_api
|
||||
|
||||
|
||||
HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnknownHandler(Exception):
|
||||
"""Exception raised when trying to handle unknown handler."""
|
||||
|
||||
|
||||
class CloudIoT:
|
||||
"""Class to manage the IoT connection."""
|
||||
|
||||
def __init__(self, cloud):
|
||||
"""Initialize the CloudIoT class."""
|
||||
self.cloud = cloud
|
||||
self.client = None
|
||||
self.close_requested = False
|
||||
self.tries = 0
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
"""Return if connected to the cloud."""
|
||||
return self.client is not None
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
"""Connect to the IoT broker."""
|
||||
if self.client is not None:
|
||||
raise RuntimeError('Cannot connect while already connected')
|
||||
|
||||
self.close_requested = False
|
||||
|
||||
hass = self.cloud.hass
|
||||
remove_hass_stop_listener = None
|
||||
|
||||
session = async_get_clientsession(self.cloud.hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _handle_hass_stop(event):
|
||||
"""Handle Home Assistant shutting down."""
|
||||
nonlocal remove_hass_stop_listener
|
||||
remove_hass_stop_listener = None
|
||||
yield from self.disconnect()
|
||||
|
||||
client = None
|
||||
disconnect_warn = None
|
||||
try:
|
||||
yield from hass.async_add_job(auth_api.check_token, self.cloud)
|
||||
|
||||
self.client = client = yield from session.ws_connect(
|
||||
self.cloud.relayer, headers={
|
||||
hdrs.AUTHORIZATION:
|
||||
'Bearer {}'.format(self.cloud.id_token)
|
||||
})
|
||||
self.tries = 0
|
||||
|
||||
remove_hass_stop_listener = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
|
||||
|
||||
_LOGGER.info('Connected')
|
||||
|
||||
while not client.closed:
|
||||
msg = yield from client.receive()
|
||||
|
||||
if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED,
|
||||
WSMsgType.CLOSING):
|
||||
disconnect_warn = 'Closed by server'
|
||||
break
|
||||
|
||||
elif msg.type != WSMsgType.TEXT:
|
||||
disconnect_warn = 'Received non-Text message: {}'.format(
|
||||
msg.type)
|
||||
break
|
||||
|
||||
try:
|
||||
msg = msg.json()
|
||||
except ValueError:
|
||||
disconnect_warn = 'Received invalid JSON.'
|
||||
break
|
||||
|
||||
_LOGGER.debug('Received message: %s', msg)
|
||||
|
||||
response = {
|
||||
'msgid': msg['msgid'],
|
||||
}
|
||||
try:
|
||||
result = yield from async_handle_message(
|
||||
hass, self.cloud, msg['handler'], msg['payload'])
|
||||
|
||||
# No response from handler
|
||||
if result is None:
|
||||
continue
|
||||
|
||||
response['payload'] = result
|
||||
|
||||
except UnknownHandler:
|
||||
response['error'] = 'unknown-handler'
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error handling message')
|
||||
response['error'] = 'exception'
|
||||
|
||||
_LOGGER.debug('Publishing message: %s', response)
|
||||
yield from client.send_json(response)
|
||||
|
||||
except auth_api.CloudError:
|
||||
_LOGGER.warning('Unable to connect: Unable to refresh token.')
|
||||
|
||||
except client_exceptions.WSServerHandshakeError as err:
|
||||
if err.code == 401:
|
||||
disconnect_warn = 'Invalid auth.'
|
||||
self.close_requested = True
|
||||
# Should we notify user?
|
||||
else:
|
||||
_LOGGER.warning('Unable to connect: %s', err)
|
||||
|
||||
except client_exceptions.ClientError as err:
|
||||
_LOGGER.warning('Unable to connect: %s', err)
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
if not self.close_requested:
|
||||
_LOGGER.exception('Unexpected error')
|
||||
|
||||
finally:
|
||||
if disconnect_warn is not None:
|
||||
_LOGGER.warning('Connection closed: %s', disconnect_warn)
|
||||
|
||||
if remove_hass_stop_listener is not None:
|
||||
remove_hass_stop_listener()
|
||||
|
||||
if client is not None:
|
||||
self.client = None
|
||||
yield from client.close()
|
||||
|
||||
if not self.close_requested:
|
||||
self.tries += 1
|
||||
|
||||
# Sleep 0, 5, 10, 15 … up to 30 seconds between retries
|
||||
yield from asyncio.sleep(
|
||||
min(30, (self.tries - 1) * 5), loop=hass.loop)
|
||||
|
||||
hass.async_add_job(self.connect())
|
||||
|
||||
@asyncio.coroutine
|
||||
def disconnect(self):
|
||||
"""Disconnect the client."""
|
||||
self.close_requested = True
|
||||
yield from self.client.close()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, cloud, handler_name, payload):
|
||||
"""Handle incoming IoT message."""
|
||||
handler = HANDLERS.get(handler_name)
|
||||
|
||||
if handler is None:
|
||||
raise UnknownHandler()
|
||||
|
||||
return (yield from handler(hass, cloud, payload))
|
||||
|
||||
|
||||
@HANDLERS.register('alexa')
|
||||
@asyncio.coroutine
|
||||
def async_handle_alexa(hass, cloud, payload):
|
||||
"""Handle an incoming IoT message for Alexa."""
|
||||
return (yield from smart_home.async_handle_message(hass, payload))
|
||||
|
||||
|
||||
@HANDLERS.register('cloud')
|
||||
@asyncio.coroutine
|
||||
def async_handle_cloud(hass, cloud, payload):
|
||||
"""Handle an incoming IoT message for cloud component."""
|
||||
action = payload['action']
|
||||
|
||||
if action == 'logout':
|
||||
yield from cloud.logout()
|
||||
_LOGGER.error('You have been logged out from Home Assistant cloud: %s',
|
||||
payload['reason'])
|
||||
else:
|
||||
_LOGGER.warning('Received unknown cloud action: %s', action)
|
||||
|
||||
return None
|
||||
@@ -1,10 +0,0 @@
|
||||
"""Utilities for the cloud integration."""
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def get_mode(hass):
|
||||
"""Return the current mode of the cloud component.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return hass.data[DOMAIN]['mode']
|
||||
@@ -8,7 +8,6 @@ from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID
|
||||
from homeassistant.setup import (
|
||||
async_prepare_setup_platform, ATTR_COMPONENT)
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.util.yaml import load_yaml, dump
|
||||
|
||||
@@ -21,7 +20,8 @@ ON_DEMAND = ('zwave')
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the config component."""
|
||||
register_built_in_panel(hass, 'config', 'Configuration', 'mdi:settings')
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
'config', 'config', 'mdi:settings')
|
||||
|
||||
@asyncio.coroutine
|
||||
def setup_panel(panel_name):
|
||||
|
||||
@@ -169,21 +169,12 @@ def async_setup(hass, config):
|
||||
params.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
# call method
|
||||
update_tasks = []
|
||||
for cover in covers:
|
||||
yield from getattr(cover, method['method'])(**params)
|
||||
|
||||
update_tasks = []
|
||||
|
||||
for cover in covers:
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.async_add_job(
|
||||
cover.async_update_ha_state(True))
|
||||
if hasattr(cover, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
update_tasks.append(cover.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
@@ -21,8 +21,8 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
|
||||
STATE_CLOSED, STATE_UNKNOWN)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN,
|
||||
valid_publish_topic, valid_subscribe_topic)
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC,
|
||||
CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -37,6 +37,8 @@ CONF_SET_POSITION_TEMPLATE = 'set_position_template'
|
||||
CONF_PAYLOAD_OPEN = 'payload_open'
|
||||
CONF_PAYLOAD_CLOSE = 'payload_close'
|
||||
CONF_PAYLOAD_STOP = 'payload_stop'
|
||||
CONF_PAYLOAD_AVAILABLE = 'payload_available'
|
||||
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
|
||||
CONF_STATE_OPEN = 'state_open'
|
||||
CONF_STATE_CLOSED = 'state_closed'
|
||||
CONF_TILT_CLOSED_POSITION = 'tilt_closed_value'
|
||||
@@ -50,6 +52,8 @@ DEFAULT_NAME = 'MQTT Cover'
|
||||
DEFAULT_PAYLOAD_OPEN = 'OPEN'
|
||||
DEFAULT_PAYLOAD_CLOSE = 'CLOSE'
|
||||
DEFAULT_PAYLOAD_STOP = 'STOP'
|
||||
DEFAULT_PAYLOAD_AVAILABLE = 'online'
|
||||
DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline'
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
DEFAULT_RETAIN = False
|
||||
DEFAULT_TILT_CLOSED_POSITION = 0
|
||||
@@ -69,11 +73,16 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_AVAILABILITY_TOPIC, default=None): valid_subscribe_topic,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_AVAILABLE): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
|
||||
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
|
||||
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
@@ -106,6 +115,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_TILT_COMMAND_TOPIC),
|
||||
config.get(CONF_TILT_STATUS_TOPIC),
|
||||
config.get(CONF_QOS),
|
||||
@@ -115,6 +125,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_PAYLOAD_OPEN),
|
||||
config.get(CONF_PAYLOAD_CLOSE),
|
||||
config.get(CONF_PAYLOAD_STOP),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
config.get(CONF_OPTIMISTIC),
|
||||
value_template,
|
||||
config.get(CONF_TILT_OPEN_POSITION),
|
||||
@@ -131,9 +143,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class MqttCover(CoverDevice):
|
||||
"""Representation of a cover that can be controlled using MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, tilt_command_topic,
|
||||
tilt_status_topic, qos, retain, state_open, state_closed,
|
||||
payload_open, payload_close, payload_stop,
|
||||
def __init__(self, name, state_topic, command_topic, availability_topic,
|
||||
tilt_command_topic, tilt_status_topic, qos, retain,
|
||||
state_open, state_closed, payload_open, payload_close,
|
||||
payload_stop, payload_available, payload_not_available,
|
||||
optimistic, value_template, tilt_open_position,
|
||||
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
|
||||
tilt_invert, position_topic, set_position_template):
|
||||
@@ -143,12 +156,16 @@ class MqttCover(CoverDevice):
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._availability_topic = availability_topic
|
||||
self._available = True if availability_topic is None else False
|
||||
self._tilt_command_topic = tilt_command_topic
|
||||
self._tilt_status_topic = tilt_status_topic
|
||||
self._qos = qos
|
||||
self._payload_open = payload_open
|
||||
self._payload_close = payload_close
|
||||
self._payload_stop = payload_stop
|
||||
self._payload_available = payload_available
|
||||
self._payload_not_available = payload_not_available
|
||||
self._state_open = state_open
|
||||
self._state_closed = state_closed
|
||||
self._retain = retain
|
||||
@@ -181,8 +198,8 @@ class MqttCover(CoverDevice):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Handle new MQTT message."""
|
||||
def state_message_received(topic, payload, qos):
|
||||
"""Handle new MQTT state messages."""
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
@@ -205,12 +222,28 @@ class MqttCover(CoverDevice):
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def availability_message_received(topic, payload, qos):
|
||||
"""Handle new MQTT availability messages."""
|
||||
if payload == self._payload_available:
|
||||
self._available = True
|
||||
elif payload == self._payload_not_available:
|
||||
self._available = False
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._state_topic is None:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
else:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
self.hass, self._state_topic,
|
||||
state_message_received, self._qos)
|
||||
|
||||
if self._availability_topic is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._availability_topic,
|
||||
availability_message_received, self._qos)
|
||||
|
||||
if self._tilt_status_topic is None:
|
||||
self._tilt_optimistic = True
|
||||
@@ -230,6 +263,11 @@ class MqttCover(CoverDevice):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if cover is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
|
||||
121
homeassistant/components/cover/rflink.py
Normal file
121
homeassistant/components/cover/rflink.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Support for Rflink Cover devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.rflink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.rflink import (
|
||||
DATA_ENTITY_GROUP_LOOKUP, DATA_ENTITY_LOOKUP,
|
||||
DEVICE_DEFAULTS_SCHEMA, EVENT_KEY_COMMAND, RflinkCommand)
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
|
||||
DEPENDENCIES = ['rflink']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CONF_ALIASES = 'aliases'
|
||||
CONF_GROUP_ALIASES = 'group_aliases'
|
||||
CONF_GROUP = 'group'
|
||||
CONF_NOGROUP_ALIASES = 'nogroup_aliases'
|
||||
CONF_DEVICE_DEFAULTS = 'device_defaults'
|
||||
CONF_DEVICES = 'devices'
|
||||
CONF_AUTOMATIC_ADD = 'automatic_add'
|
||||
CONF_FIRE_EVENT = 'fire_event'
|
||||
CONF_IGNORE_DEVICES = 'ignore_devices'
|
||||
CONF_RECONNECT_INTERVAL = 'reconnect_interval'
|
||||
CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
|
||||
CONF_WAIT_FOR_ACK = 'wait_for_ack'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
|
||||
DEVICE_DEFAULTS_SCHEMA,
|
||||
vol.Optional(CONF_DEVICES, default={}): vol.Schema({
|
||||
cv.string: {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ALIASES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_GROUP_ALIASES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_NOGROUP_ALIASES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int),
|
||||
vol.Optional(CONF_GROUP, default=True): cv.boolean,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
def devices_from_config(domain_config, hass=None):
|
||||
"""Parse configuration and add Rflink cover devices."""
|
||||
devices = []
|
||||
for device_id, config in domain_config[CONF_DEVICES].items():
|
||||
device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config)
|
||||
device = RflinkCover(device_id, hass, **device_config)
|
||||
devices.append(device)
|
||||
|
||||
# Register entity (and aliases) to listen to incoming rflink events
|
||||
# Device id and normal aliases respond to normal and group command
|
||||
hass.data[DATA_ENTITY_LOOKUP][
|
||||
EVENT_KEY_COMMAND][device_id].append(device)
|
||||
if config[CONF_GROUP]:
|
||||
hass.data[DATA_ENTITY_GROUP_LOOKUP][
|
||||
EVENT_KEY_COMMAND][device_id].append(device)
|
||||
for _id in config[CONF_ALIASES]:
|
||||
hass.data[DATA_ENTITY_LOOKUP][
|
||||
EVENT_KEY_COMMAND][_id].append(device)
|
||||
hass.data[DATA_ENTITY_GROUP_LOOKUP][
|
||||
EVENT_KEY_COMMAND][_id].append(device)
|
||||
return devices
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Rflink cover platform."""
|
||||
async_add_devices(devices_from_config(config, hass))
|
||||
|
||||
|
||||
class RflinkCover(RflinkCommand, CoverDevice):
|
||||
"""Rflink entity which can switch on/stop/off (eg: cover)."""
|
||||
|
||||
def _handle_event(self, event):
|
||||
"""Adjust state if Rflink picks up a remote command for this device."""
|
||||
self.cancel_queued_send_commands()
|
||||
|
||||
command = event['command']
|
||||
if command in ['on', 'allon']:
|
||||
self._state = True
|
||||
elif command in ['off', 'alloff']:
|
||||
self._state = False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling available in RFlink cover."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return None
|
||||
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Turn the device close."""
|
||||
return self._async_handle_command("close_cover")
|
||||
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Turn the device open."""
|
||||
return self._async_handle_command("open_cover")
|
||||
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Turn the device stop."""
|
||||
return self._async_handle_command("stop_cover")
|
||||
@@ -1,71 +1,63 @@
|
||||
open_cover:
|
||||
description: Open all or specified cover
|
||||
# Describes the format for available cover services
|
||||
|
||||
open_cover:
|
||||
description: Open all or specified cover.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of cover(s) to open
|
||||
description: Name(s) of cover(s) to open.
|
||||
example: 'cover.living_room'
|
||||
|
||||
close_cover:
|
||||
description: Close all or specified cover
|
||||
|
||||
description: Close all or specified cover.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of cover(s) to close
|
||||
description: Name(s) of cover(s) to close.
|
||||
example: 'cover.living_room'
|
||||
|
||||
set_cover_position:
|
||||
description: Move to specific position all or specified cover
|
||||
|
||||
description: Move to specific position all or specified cover.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of cover(s) to set cover position
|
||||
description: Name(s) of cover(s) to set cover position.
|
||||
example: 'cover.living_room'
|
||||
|
||||
position:
|
||||
description: Position of the cover (0 to 100)
|
||||
description: Position of the cover (0 to 100).
|
||||
example: 30
|
||||
|
||||
stop_cover:
|
||||
description: Stop all or specified cover
|
||||
|
||||
description: Stop all or specified cover.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of cover(s) to stop
|
||||
description: Name(s) of cover(s) to stop.
|
||||
example: 'cover.living_room'
|
||||
|
||||
open_cover_tilt:
|
||||
description: Open all or specified cover tilt
|
||||
|
||||
description: Open all or specified cover tilt.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of cover(s) tilt to open
|
||||
description: Name(s) of cover(s) tilt to open.
|
||||
example: 'cover.living_room'
|
||||
|
||||
close_cover_tilt:
|
||||
description: Close all or specified cover tilt
|
||||
|
||||
description: Close all or specified cover tilt.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of cover(s) to close tilt
|
||||
description: Name(s) of cover(s) to close tilt.
|
||||
example: 'cover.living_room'
|
||||
|
||||
set_cover_tilt_position:
|
||||
description: Move to specific position all or specified cover tilt
|
||||
|
||||
description: Move to specific position all or specified cover tilt.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of cover(s) to set cover tilt position
|
||||
description: Name(s) of cover(s) to set cover tilt position.
|
||||
example: 'cover.living_room'
|
||||
|
||||
position:
|
||||
description: Position of the cover (0 to 100)
|
||||
description: Position of the cover (0 to 100).
|
||||
example: 30
|
||||
|
||||
stop_cover_tilt:
|
||||
description: Stop all or specified cover
|
||||
|
||||
description: Stop all or specified cover.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of cover(s) to stop
|
||||
description: Name(s) of cover(s) to stop.
|
||||
example: 'cover.living_room'
|
||||
|
||||
@@ -19,12 +19,12 @@ from homeassistant.const import (
|
||||
CONF_FRIENDLY_NAME, CONF_ENTITY_ID,
|
||||
EVENT_HOMEASSISTANT_START, MATCH_ALL,
|
||||
CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE,
|
||||
CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED)
|
||||
CONF_ENTITY_PICTURE_TEMPLATE, CONF_OPTIMISTIC,
|
||||
STATE_OPEN, STATE_CLOSED)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.script import Script
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -58,6 +58,7 @@ COVER_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_POSITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TILT_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_ICON_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
@@ -82,6 +83,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
position_template = device_config.get(CONF_POSITION_TEMPLATE)
|
||||
tilt_template = device_config.get(CONF_TILT_TEMPLATE)
|
||||
icon_template = device_config.get(CONF_ICON_TEMPLATE)
|
||||
entity_picture_template = device_config.get(
|
||||
CONF_ENTITY_PICTURE_TEMPLATE)
|
||||
open_action = device_config.get(OPEN_ACTION)
|
||||
close_action = device_config.get(CLOSE_ACTION)
|
||||
stop_action = device_config.get(STOP_ACTION)
|
||||
@@ -115,6 +118,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if entity_picture_template is not None:
|
||||
temp_ids = entity_picture_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if not template_entity_ids:
|
||||
template_entity_ids = MATCH_ALL
|
||||
|
||||
@@ -125,8 +133,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
hass,
|
||||
device, friendly_name, state_template,
|
||||
position_template, tilt_template, icon_template,
|
||||
open_action, close_action, stop_action,
|
||||
position_action, tilt_action,
|
||||
entity_picture_template, open_action, close_action,
|
||||
stop_action, position_action, tilt_action,
|
||||
optimistic, tilt_optimistic, entity_ids
|
||||
)
|
||||
)
|
||||
@@ -134,7 +142,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
_LOGGER.error("No covers added")
|
||||
return False
|
||||
|
||||
async_add_devices(covers, True)
|
||||
async_add_devices(covers)
|
||||
return True
|
||||
|
||||
|
||||
@@ -143,8 +151,8 @@ class CoverTemplate(CoverDevice):
|
||||
|
||||
def __init__(self, hass, device_id, friendly_name, state_template,
|
||||
position_template, tilt_template, icon_template,
|
||||
open_action, close_action, stop_action,
|
||||
position_action, tilt_action,
|
||||
entity_picture_template, open_action, close_action,
|
||||
stop_action, position_action, tilt_action,
|
||||
optimistic, tilt_optimistic, entity_ids):
|
||||
"""Initialize the Template cover."""
|
||||
self.hass = hass
|
||||
@@ -155,6 +163,7 @@ class CoverTemplate(CoverDevice):
|
||||
self._position_template = position_template
|
||||
self._tilt_template = tilt_template
|
||||
self._icon_template = icon_template
|
||||
self._entity_picture_template = entity_picture_template
|
||||
self._open_script = None
|
||||
if open_action is not None:
|
||||
self._open_script = Script(hass, open_action)
|
||||
@@ -174,6 +183,7 @@ class CoverTemplate(CoverDevice):
|
||||
(not state_template and not position_template))
|
||||
self._tilt_optimistic = tilt_optimistic or not tilt_template
|
||||
self._icon = None
|
||||
self._entity_picture = None
|
||||
self._position = None
|
||||
self._tilt_value = None
|
||||
self._entities = entity_ids
|
||||
@@ -186,14 +196,12 @@ class CoverTemplate(CoverDevice):
|
||||
self._tilt_template.hass = self.hass
|
||||
if self._icon_template is not None:
|
||||
self._icon_template.hass = self.hass
|
||||
if self._entity_picture_template is not None:
|
||||
self._entity_picture_template.hass = self.hass
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._position = 100 if state.state == STATE_OPEN else 0
|
||||
|
||||
@callback
|
||||
def template_cover_state_listener(entity, old_state, new_state):
|
||||
"""Handle target device state changes."""
|
||||
@@ -241,6 +249,11 @@ class CoverTemplate(CoverDevice):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Return the entity picture to use in the frontend, if any."""
|
||||
return self._entity_picture
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
@@ -288,7 +301,7 @@ class CoverTemplate(CoverDevice):
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
if self._stop_script:
|
||||
self.hass.async_add_job(self._stop_script.async_run())
|
||||
yield from self._stop_script.async_run()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
@@ -374,16 +387,28 @@ class CoverTemplate(CoverDevice):
|
||||
except ValueError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._tilt_value = None
|
||||
if self._icon_template is not None:
|
||||
|
||||
for property_name, template in (
|
||||
('_icon', self._icon_template),
|
||||
('_entity_picture', self._entity_picture_template)):
|
||||
if template is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
self._icon = self._icon_template.async_render()
|
||||
setattr(self, property_name, template.async_render())
|
||||
except TemplateError as ex:
|
||||
friendly_property_name = property_name[1:].replace('_', ' ')
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
# Common during HA startup - so just a warning
|
||||
_LOGGER.warning('Could not render icon template %s,'
|
||||
' the state is unknown.', self._name)
|
||||
_LOGGER.warning('Could not render %s template %s,'
|
||||
' the state is unknown.',
|
||||
friendly_property_name, self._name)
|
||||
return
|
||||
self._icon = super().icon
|
||||
_LOGGER.error('Could not render icon template %s: %s',
|
||||
self._name, ex)
|
||||
|
||||
try:
|
||||
setattr(self, property_name,
|
||||
getattr(super(), property_name))
|
||||
except AttributeError:
|
||||
_LOGGER.error('Could not render %s template %s: %s',
|
||||
friendly_property_name, self._name, ex)
|
||||
|
||||
@@ -87,8 +87,8 @@ def async_setup(hass, config):
|
||||
|
||||
# Set up input boolean
|
||||
tasks.append(bootstrap.async_setup_component(
|
||||
hass, 'input_slider',
|
||||
{'input_slider': {
|
||||
hass, 'input_number',
|
||||
{'input_number': {
|
||||
'noise_allowance': {'icon': 'mdi:bell-ring',
|
||||
'min': 0,
|
||||
'max': 10,
|
||||
@@ -163,7 +163,7 @@ def async_setup(hass, config):
|
||||
'scene.romantic_lights']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'Bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance']))
|
||||
'input_number.noise_allowance']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'Kitchen', [
|
||||
lights[2], 'cover.kitchen_window', 'lock.kitchen_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'Doors', [
|
||||
|
||||
@@ -18,7 +18,6 @@ from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -89,10 +88,6 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
SERVICE_NETGEAR: 'netgear',
|
||||
}
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistantType, entity_id: str=None):
|
||||
@@ -180,15 +175,6 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
|
||||
tracker.async_setup_group()
|
||||
|
||||
@callback
|
||||
def async_device_tracker_discovered(service, info):
|
||||
"""Handle the discovery of device tracker platforms."""
|
||||
hass.async_add_job(
|
||||
async_setup_platform(DISCOVERY_PLATFORMS[service], {}, info))
|
||||
|
||||
discovery.async_listen(
|
||||
hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_platform_discovered(platform, info):
|
||||
"""Load a platform."""
|
||||
|
||||
@@ -12,18 +12,17 @@ from collections import namedtuple
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE,
|
||||
CONF_PROTOCOL)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_MODE = 'mode'
|
||||
CONF_PROTOCOL = 'protocol'
|
||||
CONF_PUB_KEY = 'pub_key'
|
||||
CONF_SSH_KEY = 'ssh_key'
|
||||
|
||||
@@ -36,10 +35,8 @@ PLATFORM_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PROTOCOL, default='ssh'):
|
||||
vol.In(['ssh', 'telnet']),
|
||||
vol.Optional(CONF_MODE, default='router'):
|
||||
vol.In(['router', 'ap']),
|
||||
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
|
||||
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
|
||||
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
|
||||
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
|
||||
@@ -102,21 +99,18 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
self.success_init = False
|
||||
return
|
||||
|
||||
self.connection = SshConnection(self.host, self.port,
|
||||
self.username,
|
||||
self.password,
|
||||
self.ssh_key,
|
||||
self.mode == "ap")
|
||||
self.connection = SshConnection(
|
||||
self.host, self.port, self.username, self.password,
|
||||
self.ssh_key, self.mode == 'ap')
|
||||
else:
|
||||
if not self.password:
|
||||
_LOGGER.error("No password specified")
|
||||
self.success_init = False
|
||||
return
|
||||
|
||||
self.connection = TelnetConnection(self.host, self.port,
|
||||
self.username,
|
||||
self.password,
|
||||
self.mode == "ap")
|
||||
self.connection = TelnetConnection(
|
||||
self.host, self.port, self.username, self.password,
|
||||
self.mode == 'ap')
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
REQUIREMENTS = ['aioautomatic==0.6.3']
|
||||
REQUIREMENTS = ['aioautomatic==0.6.4']
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,7 +35,7 @@ CONF_CURRENT_LOCATION = 'current_location'
|
||||
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
DEFAULT_SCOPE = ['location', 'vehicle:profile', 'trip']
|
||||
DEFAULT_SCOPE = ['location', 'trip', 'vehicle:events', 'vehicle:profile']
|
||||
FULL_SCOPE = DEFAULT_SCOPE + ['current_location']
|
||||
|
||||
ATTR_FUEL_LEVEL = 'fuel_level'
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||
REQUIREMENTS = ['fritzconnection==0.6.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ATTR_CURRENT_LATITUDE = 'currentLatitude'
|
||||
ATTR_CURRENT_LONGITUDE = 'currentLongitude'
|
||||
|
||||
BEACON_DEV_PREFIX = 'beacon'
|
||||
CONF_MOBILE_BEACONS = 'mobile_beacons'
|
||||
|
||||
@@ -72,6 +75,9 @@ class GeofencyView(HomeAssistantView):
|
||||
location_name = data['name']
|
||||
else:
|
||||
location_name = STATE_NOT_HOME
|
||||
if ATTR_CURRENT_LATITUDE in data:
|
||||
data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE]
|
||||
data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE]
|
||||
|
||||
return (yield from self._set_location(hass, data, location_name))
|
||||
|
||||
@@ -96,8 +102,12 @@ class GeofencyView(HomeAssistantView):
|
||||
data['device'] = slugify(data['device'])
|
||||
data['name'] = slugify(data['name'])
|
||||
|
||||
data[ATTR_LATITUDE] = float(data[ATTR_LATITUDE])
|
||||
data[ATTR_LONGITUDE] = float(data[ATTR_LONGITUDE])
|
||||
gps_attributes = [ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
ATTR_CURRENT_LATITUDE, ATTR_CURRENT_LONGITUDE]
|
||||
|
||||
for attribute in gps_attributes:
|
||||
if attribute in data:
|
||||
data[attribute] = float(data[attribute])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ class Icloud(DeviceScanner):
|
||||
self._trusted_device, self._verification_code):
|
||||
raise PyiCloudException('Unknown failure')
|
||||
except PyiCloudException as error:
|
||||
# Reset to the inital 2FA state to allow the user to retry
|
||||
# Reset to the initial 2FA state to allow the user to retry
|
||||
_LOGGER.error("Failed to verify verification code: %s", error)
|
||||
self._trusted_device = None
|
||||
self._verification_code = None
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||
|
||||
REQUIREMENTS = ['librouteros==1.0.2']
|
||||
REQUIREMENTS = ['librouteros==1.0.4']
|
||||
|
||||
MTK_DEFAULT_API_PORT = '8728'
|
||||
|
||||
@@ -76,16 +76,47 @@ class MikrotikScanner(DeviceScanner):
|
||||
port=int(self.port)
|
||||
)
|
||||
|
||||
routerboard_info = self.client(cmd='/system/routerboard/getall')
|
||||
try:
|
||||
routerboard_info = self.client(
|
||||
cmd='/system/routerboard/getall')
|
||||
except (librouteros.exceptions.TrapError,
|
||||
librouteros.exceptions.MultiTrapError,
|
||||
librouteros.exceptions.ConnectionError):
|
||||
routerboard_info = None
|
||||
raise
|
||||
|
||||
if routerboard_info:
|
||||
_LOGGER.info("Connected to Mikrotik %s with IP %s",
|
||||
routerboard_info[0].get('model', 'Router'),
|
||||
self.host)
|
||||
|
||||
self.connected = True
|
||||
self.wireless_exist = self.client(
|
||||
cmd='/interface/wireless/getall'
|
||||
)
|
||||
|
||||
try:
|
||||
self.capsman_exist = self.client(
|
||||
cmd='/caps-man/interface/getall'
|
||||
)
|
||||
except (librouteros.exceptions.TrapError,
|
||||
librouteros.exceptions.MultiTrapError,
|
||||
librouteros.exceptions.ConnectionError):
|
||||
self.capsman_exist = False
|
||||
|
||||
if not self.capsman_exist:
|
||||
_LOGGER.info(
|
||||
'Mikrotik %s: Not a CAPSman controller. Trying '
|
||||
'local interfaces ',
|
||||
self.host
|
||||
)
|
||||
|
||||
try:
|
||||
self.wireless_exist = self.client(
|
||||
cmd='/interface/wireless/getall'
|
||||
)
|
||||
except (librouteros.exceptions.TrapError,
|
||||
librouteros.exceptions.MultiTrapError,
|
||||
librouteros.exceptions.ConnectionError):
|
||||
self.wireless_exist = False
|
||||
|
||||
if not self.wireless_exist:
|
||||
_LOGGER.info(
|
||||
'Mikrotik %s: Wireless adapters not found. Try to '
|
||||
@@ -95,6 +126,7 @@ class MikrotikScanner(DeviceScanner):
|
||||
)
|
||||
|
||||
except (librouteros.exceptions.TrapError,
|
||||
librouteros.exceptions.MultiTrapError,
|
||||
librouteros.exceptions.ConnectionError) as api_error:
|
||||
_LOGGER.error("Connection error: %s", api_error)
|
||||
|
||||
@@ -111,7 +143,9 @@ class MikrotikScanner(DeviceScanner):
|
||||
|
||||
def _update_info(self):
|
||||
"""Retrieve latest information from the Mikrotik box."""
|
||||
if self.wireless_exist:
|
||||
if self.capsman_exist:
|
||||
devices_tracker = 'capsman'
|
||||
elif self.wireless_exist:
|
||||
devices_tracker = 'wireless'
|
||||
else:
|
||||
devices_tracker = 'ip'
|
||||
@@ -123,7 +157,11 @@ class MikrotikScanner(DeviceScanner):
|
||||
)
|
||||
|
||||
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
|
||||
if self.wireless_exist:
|
||||
if devices_tracker == 'capsman':
|
||||
devices = self.client(
|
||||
cmd='/caps-man/registration-table/getall'
|
||||
)
|
||||
elif devices_tracker == 'wireless':
|
||||
devices = self.client(
|
||||
cmd='/interface/wireless/registration-table/getall'
|
||||
)
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
"""
|
||||
Support the OwnTracks platform.
|
||||
Device tracker platform that adds support for OwnTracks over MQTT.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.owntracks/
|
||||
"""
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.util import convert, slugify
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import zone as zone_comp
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util import slugify, decorator
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
REQUIREMENTS = ['libnacl==1.5.2']
|
||||
REQUIREMENTS = ['libnacl==1.6.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HANDLERS = decorator.Registry()
|
||||
|
||||
BEACON_DEV_ID = 'beacon'
|
||||
|
||||
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
|
||||
@@ -32,17 +33,9 @@ CONF_SECRET = 'secret'
|
||||
CONF_WAYPOINT_IMPORT = 'waypoints'
|
||||
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
||||
|
||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
|
||||
VALIDATE_LOCATION = 'location'
|
||||
VALIDATE_TRANSITION = 'transition'
|
||||
VALIDATE_WAYPOINTS = 'waypoints'
|
||||
|
||||
WAYPOINT_LAT_KEY = 'lat'
|
||||
WAYPOINT_LON_KEY = 'lon'
|
||||
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoints'
|
||||
OWNTRACKS_TOPIC = 'owntracks/#'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
|
||||
@@ -72,300 +65,65 @@ def get_cipher():
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up an OwnTracks tracker."""
|
||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
||||
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
||||
secret = config.get(CONF_SECRET)
|
||||
context = context_from_config(async_see, config)
|
||||
|
||||
mobile_beacons_active = defaultdict(list)
|
||||
regions_entered = defaultdict(list)
|
||||
|
||||
def decrypt_payload(topic, ciphertext):
|
||||
"""Decrypt encrypted payload."""
|
||||
@asyncio.coroutine
|
||||
def async_handle_mqtt_message(topic, payload, qos):
|
||||
"""Handle incoming OwnTracks message."""
|
||||
try:
|
||||
keylen, decrypt = get_cipher()
|
||||
except OSError:
|
||||
_LOGGER.warning(
|
||||
"Ignoring encrypted payload because libsodium not installed")
|
||||
return None
|
||||
|
||||
if isinstance(secret, dict):
|
||||
key = secret.get(topic)
|
||||
else:
|
||||
key = secret
|
||||
|
||||
if key is None:
|
||||
_LOGGER.warning(
|
||||
"Ignoring encrypted payload because no decryption key known "
|
||||
"for topic %s", topic)
|
||||
return None
|
||||
|
||||
key = key.encode("utf-8")
|
||||
key = key[:keylen]
|
||||
key = key.ljust(keylen, b'\0')
|
||||
|
||||
try:
|
||||
ciphertext = base64.b64decode(ciphertext)
|
||||
message = decrypt(ciphertext, key)
|
||||
message = message.decode("utf-8")
|
||||
_LOGGER.debug("Decrypted payload: %s", message)
|
||||
return message
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Ignoring encrypted payload because unable to decrypt using "
|
||||
"key for topic %s", topic)
|
||||
return None
|
||||
|
||||
def validate_payload(topic, payload, data_type):
|
||||
"""Validate the OwnTracks payload."""
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
message = json.loads(payload)
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
_LOGGER.error("Unable to parse payload as JSON: %s", payload)
|
||||
return None
|
||||
|
||||
if isinstance(data, dict) and \
|
||||
data.get('_type') == 'encrypted' and \
|
||||
'data' in data:
|
||||
plaintext_payload = decrypt_payload(topic, data['data'])
|
||||
if plaintext_payload is None:
|
||||
return None
|
||||
return validate_payload(topic, plaintext_payload, data_type)
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != data_type:
|
||||
_LOGGER.debug("Skipping %s update for following data "
|
||||
"because of missing or malformatted data: %s",
|
||||
data_type, data)
|
||||
return None
|
||||
if data_type == VALIDATE_TRANSITION or data_type == VALIDATE_WAYPOINTS:
|
||||
return data
|
||||
if max_gps_accuracy is not None and \
|
||||
convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
|
||||
_LOGGER.info("Ignoring %s update because expected GPS "
|
||||
"accuracy %s is not met: %s",
|
||||
data_type, max_gps_accuracy, payload)
|
||||
return None
|
||||
if convert(data.get('acc'), float, 1.0) == 0.0:
|
||||
_LOGGER.warning(
|
||||
"Ignoring %s update because GPS accuracy is zero: %s",
|
||||
data_type, payload)
|
||||
return None
|
||||
|
||||
return data
|
||||
|
||||
@callback
|
||||
def async_owntracks_location_update(topic, payload, qos):
|
||||
"""MQTT message received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||
data = validate_payload(topic, payload, VALIDATE_LOCATION)
|
||||
if not data:
|
||||
return
|
||||
|
||||
dev_id, kwargs = _parse_see_args(topic, data)
|
||||
message['topic'] = topic
|
||||
|
||||
if regions_entered[dev_id]:
|
||||
_LOGGER.debug(
|
||||
"Location update ignored, inside region %s",
|
||||
regions_entered[-1])
|
||||
return
|
||||
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
@callback
|
||||
def async_owntracks_event_update(topic, payload, qos):
|
||||
"""Handle MQTT event (geofences)."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||
data = validate_payload(topic, payload, VALIDATE_TRANSITION)
|
||||
if not data:
|
||||
return
|
||||
|
||||
if data.get('desc') is None:
|
||||
_LOGGER.error(
|
||||
"Location missing from `Entering/Leaving` message - "
|
||||
"please turn `Share` on in OwnTracks app")
|
||||
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)
|
||||
|
||||
def enter_event():
|
||||
"""Execute enter event."""
|
||||
zone = hass.states.get("zone.{}".format(slugify(location)))
|
||||
if zone is None and data.get('t') == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
beacons = mobile_beacons_active[dev_id]
|
||||
if location not in beacons:
|
||||
beacons.append(location)
|
||||
_LOGGER.info("Added beacon %s", location)
|
||||
else:
|
||||
# Normal region
|
||||
regions = regions_entered[dev_id]
|
||||
if location not in regions:
|
||||
regions.append(location)
|
||||
_LOGGER.info("Enter region %s", location)
|
||||
_set_gps_from_zone(kwargs, location, zone)
|
||||
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
def leave_event():
|
||||
"""Execute leave event."""
|
||||
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(slugify(new_region)))
|
||||
_set_gps_from_zone(kwargs, new_region, zone)
|
||||
_LOGGER.info("Exit to %s", new_region)
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
# Check for GPS accuracy
|
||||
valid_gps = True
|
||||
if 'acc' in data:
|
||||
if data['acc'] == 0.0:
|
||||
valid_gps = False
|
||||
_LOGGER.warning(
|
||||
"Ignoring GPS in region exit because accuracy"
|
||||
"is zero: %s", payload)
|
||||
if (max_gps_accuracy is not None and
|
||||
data['acc'] > max_gps_accuracy):
|
||||
valid_gps = False
|
||||
_LOGGER.info(
|
||||
"Ignoring GPS in region exit because expected "
|
||||
"GPS accuracy %s is not met: %s",
|
||||
max_gps_accuracy, payload)
|
||||
if valid_gps:
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
beacons = mobile_beacons_active[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
_LOGGER.info("Remove beacon %s", location)
|
||||
|
||||
if data['event'] == 'enter':
|
||||
enter_event()
|
||||
elif data['event'] == 'leave':
|
||||
leave_event()
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Misformatted mqtt msgs, _type=transition, event=%s",
|
||||
data['event'])
|
||||
return
|
||||
|
||||
@callback
|
||||
def async_owntracks_waypoint_update(topic, payload, qos):
|
||||
"""List of waypoints published by a user."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typewaypoints
|
||||
data = validate_payload(topic, payload, VALIDATE_WAYPOINTS)
|
||||
if not data:
|
||||
return
|
||||
|
||||
wayps = data['waypoints']
|
||||
_LOGGER.info("Got %d waypoints from %s", len(wayps), topic)
|
||||
for wayp in wayps:
|
||||
name = wayp['desc']
|
||||
pretty_name = parse_topic(topic, True)[1] + ' - ' + name
|
||||
lat = wayp[WAYPOINT_LAT_KEY]
|
||||
lon = wayp[WAYPOINT_LON_KEY]
|
||||
rad = wayp['rad']
|
||||
|
||||
# check zone exists
|
||||
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
|
||||
|
||||
# Check if state already exists
|
||||
if hass.states.get(entity_id) is not None:
|
||||
continue
|
||||
|
||||
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
||||
zone_comp.ICON_IMPORT, False)
|
||||
zone.entity_id = entity_id
|
||||
hass.async_add_job(zone.async_update_ha_state())
|
||||
|
||||
@callback
|
||||
def async_see_beacons(dev_id, kwargs_param):
|
||||
"""Set active beacons to the current location."""
|
||||
kwargs = kwargs_param.copy()
|
||||
# the battery state applies to the tracking device, not the beacon
|
||||
kwargs.pop('battery', None)
|
||||
for beacon in mobile_beacons_active[dev_id]:
|
||||
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
|
||||
kwargs['host_name'] = beacon
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
yield from async_handle_message(hass, context, message)
|
||||
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, LOCATION_TOPIC, async_owntracks_location_update, 1)
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, EVENT_TOPIC, async_owntracks_event_update, 1)
|
||||
|
||||
if waypoint_import:
|
||||
if waypoint_whitelist is None:
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, WAYPOINT_TOPIC.format('+', '+'),
|
||||
async_owntracks_waypoint_update, 1)
|
||||
else:
|
||||
for whitelist_user in waypoint_whitelist:
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, WAYPOINT_TOPIC.format(whitelist_user, '+'),
|
||||
async_owntracks_waypoint_update, 1)
|
||||
hass, OWNTRACKS_TOPIC, async_handle_mqtt_message, 1)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def parse_topic(topic, pretty=False):
|
||||
def _parse_topic(topic):
|
||||
"""Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
parts = topic.split('/')
|
||||
dev_id_format = ''
|
||||
if pretty:
|
||||
dev_id_format = '{} {}'
|
||||
else:
|
||||
dev_id_format = '{}_{}'
|
||||
dev_id = slugify(dev_id_format.format(parts[1], parts[2]))
|
||||
host_name = parts[1]
|
||||
return (host_name, dev_id)
|
||||
try:
|
||||
_, user, device, *_ = topic.split('/', 3)
|
||||
except ValueError:
|
||||
_LOGGER.error("Can't parse topic: '%s'", topic)
|
||||
raise
|
||||
|
||||
return user, device
|
||||
|
||||
|
||||
def _parse_see_args(topic, data):
|
||||
def _parse_see_args(message):
|
||||
"""Parse the OwnTracks location parameters, into the format see expects.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
(host_name, dev_id) = parse_topic(topic, False)
|
||||
user, device = _parse_topic(message['topic'])
|
||||
dev_id = slugify('{}_{}'.format(user, device))
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
'host_name': host_name,
|
||||
'gps': (data[WAYPOINT_LAT_KEY], data[WAYPOINT_LON_KEY]),
|
||||
'host_name': user,
|
||||
'gps': (message['lat'], message['lon']),
|
||||
'attributes': {}
|
||||
}
|
||||
if 'acc' in data:
|
||||
kwargs['gps_accuracy'] = data['acc']
|
||||
if 'batt' in data:
|
||||
kwargs['battery'] = data['batt']
|
||||
if 'vel' in data:
|
||||
kwargs['attributes']['velocity'] = data['vel']
|
||||
if 'tid' in data:
|
||||
kwargs['attributes']['tid'] = data['tid']
|
||||
if 'addr' in data:
|
||||
kwargs['attributes']['address'] = data['addr']
|
||||
if 'acc' in message:
|
||||
kwargs['gps_accuracy'] = message['acc']
|
||||
if 'batt' in message:
|
||||
kwargs['battery'] = message['batt']
|
||||
if 'vel' in message:
|
||||
kwargs['attributes']['velocity'] = message['vel']
|
||||
if 'tid' in message:
|
||||
kwargs['attributes']['tid'] = message['tid']
|
||||
if 'addr' in message:
|
||||
kwargs['attributes']['address'] = message['addr']
|
||||
|
||||
return dev_id, kwargs
|
||||
|
||||
@@ -382,3 +140,305 @@ def _set_gps_from_zone(kwargs, location, zone):
|
||||
kwargs['gps_accuracy'] = zone.attributes['radius']
|
||||
kwargs['location_name'] = location
|
||||
return kwargs
|
||||
|
||||
|
||||
def _decrypt_payload(secret, topic, ciphertext):
|
||||
"""Decrypt encrypted payload."""
|
||||
try:
|
||||
keylen, decrypt = get_cipher()
|
||||
except OSError:
|
||||
_LOGGER.warning(
|
||||
"Ignoring encrypted payload because libsodium not installed")
|
||||
return None
|
||||
|
||||
if isinstance(secret, dict):
|
||||
key = secret.get(topic)
|
||||
else:
|
||||
key = secret
|
||||
|
||||
if key is None:
|
||||
_LOGGER.warning(
|
||||
"Ignoring encrypted payload because no decryption key known "
|
||||
"for topic %s", topic)
|
||||
return None
|
||||
|
||||
key = key.encode("utf-8")
|
||||
key = key[:keylen]
|
||||
key = key.ljust(keylen, b'\0')
|
||||
|
||||
try:
|
||||
ciphertext = base64.b64decode(ciphertext)
|
||||
message = decrypt(ciphertext, key)
|
||||
message = message.decode("utf-8")
|
||||
_LOGGER.debug("Decrypted payload: %s", message)
|
||||
return message
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Ignoring encrypted payload because unable to decrypt using "
|
||||
"key for topic %s", topic)
|
||||
return None
|
||||
|
||||
|
||||
def context_from_config(async_see, config):
|
||||
"""Create an async context from Home Assistant config."""
|
||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
||||
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
||||
secret = config.get(CONF_SECRET)
|
||||
|
||||
return OwnTracksContext(async_see, secret, max_gps_accuracy,
|
||||
waypoint_import, waypoint_whitelist)
|
||||
|
||||
|
||||
class OwnTracksContext:
|
||||
"""Hold the current OwnTracks context."""
|
||||
|
||||
def __init__(self, async_see, secret, max_gps_accuracy, import_waypoints,
|
||||
waypoint_whitelist):
|
||||
"""Initialize an OwnTracks context."""
|
||||
self.async_see = async_see
|
||||
self.secret = secret
|
||||
self.max_gps_accuracy = max_gps_accuracy
|
||||
self.mobile_beacons_active = defaultdict(set)
|
||||
self.regions_entered = defaultdict(list)
|
||||
self.import_waypoints = import_waypoints
|
||||
self.waypoint_whitelist = waypoint_whitelist
|
||||
|
||||
@callback
|
||||
def async_valid_accuracy(self, message):
|
||||
"""Check if we should ignore this message."""
|
||||
acc = message.get('acc')
|
||||
|
||||
if acc is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
acc = float(acc)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
if acc == 0:
|
||||
_LOGGER.warning(
|
||||
"Ignoring %s update because GPS accuracy is zero: %s",
|
||||
message['_type'], message)
|
||||
return False
|
||||
|
||||
if self.max_gps_accuracy is not None and \
|
||||
acc > self.max_gps_accuracy:
|
||||
_LOGGER.info("Ignoring %s update because expected GPS "
|
||||
"accuracy %s is not met: %s",
|
||||
message['_type'], self.max_gps_accuracy,
|
||||
message)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_see_beacons(self, hass, dev_id, kwargs_param):
|
||||
"""Set active beacons to the current location."""
|
||||
kwargs = kwargs_param.copy()
|
||||
|
||||
# Mobile beacons should always be set to the location of the
|
||||
# tracking device. I get the device state and make the necessary
|
||||
# changes to kwargs.
|
||||
device_tracker_state = hass.states.get(
|
||||
"device_tracker.{}".format(dev_id))
|
||||
|
||||
if device_tracker_state is not None:
|
||||
acc = device_tracker_state.attributes.get("gps_accuracy")
|
||||
lat = device_tracker_state.attributes.get("latitude")
|
||||
lon = device_tracker_state.attributes.get("longitude")
|
||||
kwargs['gps_accuracy'] = acc
|
||||
kwargs['gps'] = (lat, lon)
|
||||
|
||||
# the battery state applies to the tracking device, not the beacon
|
||||
# kwargs location is the beacon's configured lat/lon
|
||||
kwargs.pop('battery', None)
|
||||
for beacon in self.mobile_beacons_active[dev_id]:
|
||||
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
|
||||
kwargs['host_name'] = beacon
|
||||
yield from self.async_see(**kwargs)
|
||||
|
||||
|
||||
@HANDLERS.register('location')
|
||||
@asyncio.coroutine
|
||||
def async_handle_location_message(hass, context, message):
|
||||
"""Handle a location message."""
|
||||
if not context.async_valid_accuracy(message):
|
||||
return
|
||||
|
||||
dev_id, kwargs = _parse_see_args(message)
|
||||
|
||||
if context.regions_entered[dev_id]:
|
||||
_LOGGER.debug(
|
||||
"Location update ignored, inside region %s",
|
||||
context.regions_entered[-1])
|
||||
return
|
||||
|
||||
yield from context.async_see(**kwargs)
|
||||
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_transition_message_enter(hass, context, message, location):
|
||||
"""Execute enter event."""
|
||||
zone = hass.states.get("zone.{}".format(slugify(location)))
|
||||
dev_id, kwargs = _parse_see_args(message)
|
||||
|
||||
if zone is None and message.get('t') == 'b':
|
||||
# Not a HA zone, and a beacon so mobile beacon.
|
||||
# kwargs will contain the lat/lon of the beacon
|
||||
# which is not where the beacon actually is
|
||||
# and is probably set to 0/0
|
||||
beacons = context.mobile_beacons_active[dev_id]
|
||||
if location not in beacons:
|
||||
beacons.add(location)
|
||||
_LOGGER.info("Added beacon %s", location)
|
||||
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
||||
else:
|
||||
# Normal region
|
||||
regions = context.regions_entered[dev_id]
|
||||
if location not in regions:
|
||||
regions.append(location)
|
||||
_LOGGER.info("Enter region %s", location)
|
||||
_set_gps_from_zone(kwargs, location, zone)
|
||||
yield from context.async_see(**kwargs)
|
||||
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_transition_message_leave(hass, context, message, location):
|
||||
"""Execute leave event."""
|
||||
dev_id, kwargs = _parse_see_args(message)
|
||||
regions = context.regions_entered[dev_id]
|
||||
|
||||
if location in regions:
|
||||
regions.remove(location)
|
||||
|
||||
beacons = context.mobile_beacons_active[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
_LOGGER.info("Remove beacon %s", location)
|
||||
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
||||
else:
|
||||
new_region = regions[-1] if regions else None
|
||||
if new_region:
|
||||
# Exit to previous region
|
||||
zone = hass.states.get(
|
||||
"zone.{}".format(slugify(new_region)))
|
||||
_set_gps_from_zone(kwargs, new_region, zone)
|
||||
_LOGGER.info("Exit to %s", new_region)
|
||||
yield from context.async_see(**kwargs)
|
||||
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
||||
return
|
||||
|
||||
_LOGGER.info("Exit to GPS")
|
||||
|
||||
# Check for GPS accuracy
|
||||
if context.async_valid_accuracy(message):
|
||||
yield from context.async_see(**kwargs)
|
||||
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
||||
|
||||
|
||||
@HANDLERS.register('transition')
|
||||
@asyncio.coroutine
|
||||
def async_handle_transition_message(hass, context, message):
|
||||
"""Handle a transition message."""
|
||||
if message.get('desc') is None:
|
||||
_LOGGER.error(
|
||||
"Location missing from `Entering/Leaving` message - "
|
||||
"please turn `Share` on in OwnTracks app")
|
||||
return
|
||||
# OwnTracks uses - at the start of a beacon zone
|
||||
# to switch on 'hold mode' - ignore this
|
||||
location = message['desc'].lstrip("-")
|
||||
if location.lower() == 'home':
|
||||
location = STATE_HOME
|
||||
|
||||
if message['event'] == 'enter':
|
||||
yield from _async_transition_message_enter(
|
||||
hass, context, message, location)
|
||||
elif message['event'] == 'leave':
|
||||
yield from _async_transition_message_leave(
|
||||
hass, context, message, location)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Misformatted mqtt msgs, _type=transition, event=%s",
|
||||
message['event'])
|
||||
|
||||
|
||||
@HANDLERS.register('waypoints')
|
||||
@asyncio.coroutine
|
||||
def async_handle_waypoints_message(hass, context, message):
|
||||
"""Handle a waypoints message."""
|
||||
if not context.import_waypoints:
|
||||
return
|
||||
|
||||
if context.waypoint_whitelist is not None:
|
||||
user = _parse_topic(message['topic'])[0]
|
||||
|
||||
if user not in context.waypoint_whitelist:
|
||||
return
|
||||
|
||||
wayps = message['waypoints']
|
||||
|
||||
_LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic'])
|
||||
|
||||
name_base = ' '.join(_parse_topic(message['topic']))
|
||||
|
||||
for wayp in wayps:
|
||||
name = wayp['desc']
|
||||
pretty_name = '{} - {}'.format(name_base, name)
|
||||
lat = wayp['lat']
|
||||
lon = wayp['lon']
|
||||
rad = wayp['rad']
|
||||
|
||||
# check zone exists
|
||||
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
|
||||
|
||||
# Check if state already exists
|
||||
if hass.states.get(entity_id) is not None:
|
||||
continue
|
||||
|
||||
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
||||
zone_comp.ICON_IMPORT, False)
|
||||
zone.entity_id = entity_id
|
||||
yield from zone.async_update_ha_state()
|
||||
|
||||
|
||||
@HANDLERS.register('encrypted')
|
||||
@asyncio.coroutine
|
||||
def async_handle_encrypted_message(hass, context, message):
|
||||
"""Handle an encrypted message."""
|
||||
plaintext_payload = _decrypt_payload(context.secret, message['topic'],
|
||||
message['data'])
|
||||
|
||||
if plaintext_payload is None:
|
||||
return
|
||||
|
||||
decrypted = json.loads(plaintext_payload)
|
||||
decrypted['topic'] = message['topic']
|
||||
|
||||
yield from async_handle_message(hass, context, decrypted)
|
||||
|
||||
|
||||
@HANDLERS.register('lwt')
|
||||
@asyncio.coroutine
|
||||
def async_handle_lwt_message(hass, context, message):
|
||||
"""Handle an lwt message."""
|
||||
_LOGGER.debug('Not handling lwt message: %s', message)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, context, message):
|
||||
"""Handle an OwnTracks message."""
|
||||
msgtype = message.get('_type')
|
||||
|
||||
handler = HANDLERS.get(msgtype)
|
||||
|
||||
if handler is None:
|
||||
_LOGGER.warning(
|
||||
'Received unsupported message type: %s.', msgtype)
|
||||
return
|
||||
|
||||
yield from handler(hass, context, message)
|
||||
|
||||
54
homeassistant/components/device_tracker/owntracks_http.py
Normal file
54
homeassistant/components/device_tracker/owntracks_http.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Device tracker platform that adds support for OwnTracks over HTTP.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.owntracks_http/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from aiohttp.web_exceptions import HTTPInternalServerError
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from .owntracks import ( # NOQA
|
||||
REQUIREMENTS, PLATFORM_SCHEMA, context_from_config, async_handle_message)
|
||||
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up an OwnTracks tracker."""
|
||||
context = context_from_config(async_see, config)
|
||||
|
||||
hass.http.register_view(OwnTracksView(context))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class OwnTracksView(HomeAssistantView):
|
||||
"""View to handle OwnTracks HTTP requests."""
|
||||
|
||||
url = '/api/owntracks/{user}/{device}'
|
||||
name = 'api:owntracks'
|
||||
|
||||
def __init__(self, context):
|
||||
"""Initialize OwnTracks URL endpoints."""
|
||||
self.context = context
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, user, device):
|
||||
"""Handle an OwnTracks message."""
|
||||
hass = request.app['hass']
|
||||
|
||||
message = yield from request.json()
|
||||
message['topic'] = 'owntracks/{}/{}'.format(user, device)
|
||||
|
||||
try:
|
||||
yield from async_handle_message(hass, self.context, message)
|
||||
return self.json([])
|
||||
|
||||
except ValueError:
|
||||
raise HTTPInternalServerError
|
||||
@@ -1,41 +1,33 @@
|
||||
# Describes the format for available device tracker services
|
||||
|
||||
see:
|
||||
description: Control tracked device
|
||||
|
||||
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)
|
||||
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)
|
||||
description: Name of location where device is located (not_home is away).
|
||||
example: 'home'
|
||||
|
||||
gps:
|
||||
description: GPS coordinates where device is located (latitude, longitude)
|
||||
description: GPS coordinates where device is located (latitude, longitude).
|
||||
example: '[51.509802, -0.086692]'
|
||||
|
||||
gps_accuracy:
|
||||
description: Accuracy of GPS coordinates
|
||||
description: Accuracy of GPS coordinates.
|
||||
example: '80'
|
||||
|
||||
battery:
|
||||
description: Battery level of device
|
||||
description: Battery level of device.
|
||||
example: '100'
|
||||
|
||||
icloud:
|
||||
icloud_lost_iphone:
|
||||
description: Service to play the lost iphone sound on an iDevice
|
||||
|
||||
description: Service to play the lost iphone sound on an iDevice.
|
||||
fields:
|
||||
account_name:
|
||||
description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts.
|
||||
@@ -43,10 +35,8 @@ icloud:
|
||||
device_name:
|
||||
description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account.
|
||||
example: 'iphonebart'
|
||||
|
||||
icloud_set_interval:
|
||||
description: Service to set the interval of an iDevice
|
||||
|
||||
description: Service to set the interval of an iDevice.
|
||||
fields:
|
||||
account_name:
|
||||
description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts.
|
||||
@@ -57,10 +47,8 @@ icloud:
|
||||
interval:
|
||||
description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state.
|
||||
example: 1
|
||||
|
||||
icloud_update:
|
||||
description: Service to ask for an update of an iDevice.
|
||||
|
||||
fields:
|
||||
account_name:
|
||||
description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts.
|
||||
@@ -68,10 +56,8 @@ icloud:
|
||||
device_name:
|
||||
description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account.
|
||||
example: 'iphonebart'
|
||||
|
||||
icloud_reset_account:
|
||||
description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device.
|
||||
|
||||
fields:
|
||||
account_name:
|
||||
description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts.
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import CONF_HOST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.3.9']
|
||||
REQUIREMENTS = ['pysnmp==4.4.1']
|
||||
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
@@ -26,17 +26,17 @@ CONF_BASEOID = 'baseoid'
|
||||
DEFAULT_COMMUNITY = 'public'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_BASEOID): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
|
||||
vol.Inclusive(CONF_AUTHKEY, 'keys'): cv.string,
|
||||
vol.Inclusive(CONF_PRIVKEY, 'keys'): cv.string,
|
||||
vol.Required(CONF_BASEOID): cv.string
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an snmp scanner."""
|
||||
"""Validate the configuration and return an SNMP scanner."""
|
||||
scanner = SnmpScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
@@ -75,7 +75,7 @@ class SnmpScanner(DeviceScanner):
|
||||
return [client['mac'] for client in self.last_results
|
||||
if client.get('mac')]
|
||||
|
||||
# Supressing no-self-use warning
|
||||
# Suppressing no-self-use warning
|
||||
# pylint: disable=R0201
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
|
||||
@@ -19,16 +19,29 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DHCP_SOFTWARE = 'dhcp_software'
|
||||
DEFAULT_DHCP_SOFTWARE = 'dnsmasq'
|
||||
DHCP_SOFTWARES = [
|
||||
'dnsmasq',
|
||||
'odhcpd'
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_DHCP_SOFTWARE,
|
||||
default=DEFAULT_DHCP_SOFTWARE): vol.In(DHCP_SOFTWARES)
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an ubus scanner."""
|
||||
scanner = UbusDeviceScanner(config[DOMAIN])
|
||||
dhcp_sw = config[DOMAIN][CONF_DHCP_SOFTWARE]
|
||||
if dhcp_sw == 'dnsmasq':
|
||||
scanner = DnsmasqUbusDeviceScanner(config[DOMAIN])
|
||||
else:
|
||||
scanner = OdhcpdUbusDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
@@ -70,7 +83,6 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
self.session_id = _get_session_id(self.url, self.username,
|
||||
self.password)
|
||||
self.hostapd = []
|
||||
self.leasefile = None
|
||||
self.mac2name = None
|
||||
self.success_init = self.session_id is not None
|
||||
|
||||
@@ -79,44 +91,29 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
self._update_info()
|
||||
return self.last_results
|
||||
|
||||
def _generate_mac2name(self):
|
||||
"""Must be implemented depending on the software."""
|
||||
raise NotImplementedError
|
||||
|
||||
@_refresh_on_acccess_denied
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if self.leasefile is None:
|
||||
result = _req_json_rpc(
|
||||
self.url, self.session_id, 'call', 'uci', 'get',
|
||||
config="dhcp", type="dnsmasq")
|
||||
if result:
|
||||
values = result["values"].values()
|
||||
self.leasefile = next(iter(values))["leasefile"]
|
||||
else:
|
||||
return
|
||||
|
||||
if self.mac2name is None:
|
||||
result = _req_json_rpc(
|
||||
self.url, self.session_id, 'call', 'file', 'read',
|
||||
path=self.leasefile)
|
||||
if result:
|
||||
self.mac2name = dict()
|
||||
for line in result["data"].splitlines():
|
||||
hosts = line.split(" ")
|
||||
self.mac2name[hosts[1].upper()] = hosts[3]
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
|
||||
return self.mac2name.get(mac.upper(), None)
|
||||
self._generate_mac2name()
|
||||
name = self.mac2name.get(mac.upper(), None)
|
||||
self.mac2name = None
|
||||
return name
|
||||
|
||||
@_refresh_on_acccess_denied
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the Luci router is up to date.
|
||||
"""Ensure the information from the router is up to date.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
_LOGGER.info("Checking ARP")
|
||||
_LOGGER.info("Checking hostapd")
|
||||
|
||||
if not self.hostapd:
|
||||
hostapd = _req_json_rpc(
|
||||
@@ -136,6 +133,57 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
return bool(results)
|
||||
|
||||
|
||||
class DnsmasqUbusDeviceScanner(UbusDeviceScanner):
|
||||
"""Implement the Ubus device scanning for the dnsmasq DHCP server."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
super(DnsmasqUbusDeviceScanner, self).__init__(config)
|
||||
self.leasefile = None
|
||||
|
||||
def _generate_mac2name(self):
|
||||
if self.leasefile is None:
|
||||
result = _req_json_rpc(
|
||||
self.url, self.session_id, 'call', 'uci', 'get',
|
||||
config="dhcp", type="dnsmasq")
|
||||
if result:
|
||||
values = result["values"].values()
|
||||
self.leasefile = next(iter(values))["leasefile"]
|
||||
else:
|
||||
return
|
||||
|
||||
result = _req_json_rpc(
|
||||
self.url, self.session_id, 'call', 'file', 'read',
|
||||
path=self.leasefile)
|
||||
if result:
|
||||
self.mac2name = dict()
|
||||
for line in result["data"].splitlines():
|
||||
hosts = line.split(" ")
|
||||
self.mac2name[hosts[1].upper()] = hosts[3]
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
|
||||
|
||||
class OdhcpdUbusDeviceScanner(UbusDeviceScanner):
|
||||
"""Implement the Ubus device scanning for the odhcp DHCP server."""
|
||||
|
||||
def _generate_mac2name(self):
|
||||
result = _req_json_rpc(
|
||||
self.url, self.session_id, 'call', 'dhcp', 'ipv4leases')
|
||||
if result:
|
||||
self.mac2name = dict()
|
||||
for device in result["device"].values():
|
||||
for lease in device['leases']:
|
||||
mac = lease['mac'] # mac = aabbccddeeff
|
||||
# Convert it to expected format with colon
|
||||
mac = ":".join(mac[i:i+2] for i in range(0, len(mac), 2))
|
||||
self.mac2name[mac.upper()] = lease['hostname']
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
|
||||
|
||||
def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
|
||||
"""Perform one JSON RPC operation."""
|
||||
data = json.dumps({"jsonrpc": "2.0",
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.unifi/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -12,16 +13,19 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import CONF_VERIFY_SSL
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['pyunifi==2.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
CONF_SITE_ID = 'site_id'
|
||||
CONF_DETECTION_TIME = 'detection_time'
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 8443
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
DEFAULT_DETECTION_TIME = timedelta(seconds=300)
|
||||
|
||||
NOTIFICATION_ID = 'unifi_notification'
|
||||
NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
|
||||
@@ -32,7 +36,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): vol.Any(
|
||||
cv.boolean, cv.isfile),
|
||||
vol.Optional(CONF_DETECTION_TIME, default=DEFAULT_DETECTION_TIME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
|
||||
|
||||
@@ -46,6 +53,7 @@ def get_scanner(hass, config):
|
||||
site_id = config[DOMAIN].get(CONF_SITE_ID)
|
||||
port = config[DOMAIN].get(CONF_PORT)
|
||||
verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL)
|
||||
detection_time = config[DOMAIN].get(CONF_DETECTION_TIME)
|
||||
|
||||
try:
|
||||
ctrl = Controller(host, username, password, port, version='v4',
|
||||
@@ -61,14 +69,15 @@ def get_scanner(hass, config):
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
return UnifiScanner(ctrl)
|
||||
return UnifiScanner(ctrl, detection_time)
|
||||
|
||||
|
||||
class UnifiScanner(DeviceScanner):
|
||||
"""Provide device_tracker support from Unifi WAP client data."""
|
||||
|
||||
def __init__(self, controller):
|
||||
def __init__(self, controller, detection_time: timedelta):
|
||||
"""Initialize the scanner."""
|
||||
self._detection_time = detection_time
|
||||
self._controller = controller
|
||||
self._update()
|
||||
|
||||
@@ -81,7 +90,11 @@ class UnifiScanner(DeviceScanner):
|
||||
_LOGGER.error("Failed to scan clients: %s", ex)
|
||||
clients = []
|
||||
|
||||
self._clients = {client['mac']: client for client in clients}
|
||||
self._clients = {
|
||||
client['mac']: client
|
||||
for client in clients
|
||||
if (dt_util.utcnow() - dt_util.utc_from_timestamp(float(
|
||||
client['last_seen']))) < self._detection_time}
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for devices."""
|
||||
@@ -96,5 +109,5 @@ class UnifiScanner(DeviceScanner):
|
||||
"""
|
||||
client = self._clients.get(mac, {})
|
||||
name = client.get('name') or client.get('hostname')
|
||||
_LOGGER.debug("Device %s name %s", mac, name)
|
||||
_LOGGER.debug("Device mac %s name %s", mac, name)
|
||||
return name
|
||||
|
||||
@@ -6,7 +6,6 @@ https://home-assistant.io/components/device_tracker.upc_connect/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
@@ -19,6 +18,8 @@ from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
|
||||
REQUIREMENTS = ['defusedxml==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_IP = '192.168.0.1'
|
||||
@@ -63,6 +64,8 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
@asyncio.coroutine
|
||||
def async_scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
if self.token is None:
|
||||
token_initialized = yield from self.async_initialize_token()
|
||||
if not token_initialized:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user