mirror of
https://github.com/home-assistant/core.git
synced 2026-01-02 20:14:30 +01:00
Compare commits
542 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9a6d9ee73 | ||
|
|
425c027085 | ||
|
|
35699273da | ||
|
|
b86110a15d | ||
|
|
e449ceeeff | ||
|
|
bf8e2bd77e | ||
|
|
0202e966ea | ||
|
|
b3d66e5881 | ||
|
|
eb8a8f6d0b | ||
|
|
62c8843956 | ||
|
|
1bb37aff0c | ||
|
|
f052a0926b | ||
|
|
24aeea5ca3 | ||
|
|
5c20cc32b5 | ||
|
|
6cf2e758a8 | ||
|
|
aa6b37912a | ||
|
|
693d32fa68 | ||
|
|
072ed7ea13 | ||
|
|
bd5a16d70b | ||
|
|
eb7643e163 | ||
|
|
79ca93f892 | ||
|
|
3dbae5ca5b | ||
|
|
1719fa7008 | ||
|
|
d4bd4c114b | ||
|
|
f494c32866 | ||
|
|
e20fd3b973 | ||
|
|
270846c2f5 | ||
|
|
b2ab4443a7 | ||
|
|
17cd64966d | ||
|
|
48181a9388 | ||
|
|
d5cba0b716 | ||
|
|
3a0c749a12 | ||
|
|
d652d793f3 | ||
|
|
87995ad62c | ||
|
|
c2d0c8fba4 | ||
|
|
c7b0f25eae | ||
|
|
d5b170f761 | ||
|
|
ea7ffff0ca | ||
|
|
0cd3271dfa | ||
|
|
7920ddda9d | ||
|
|
1e493dcb8a | ||
|
|
8111e3944c | ||
|
|
8d91de877a | ||
|
|
0b4de54725 | ||
|
|
309e493e76 | ||
|
|
95c831d5bc | ||
|
|
061253fded | ||
|
|
e947e6a143 | ||
|
|
dc6e50c39d | ||
|
|
637b058a7e | ||
|
|
d25f676711 | ||
|
|
b1afed9e52 | ||
|
|
7c24d77031 | ||
|
|
e33451e2b9 | ||
|
|
2dcde12d38 | ||
|
|
3c135deec8 | ||
|
|
6974f2366d | ||
|
|
a6d9c7a621 | ||
|
|
46fe9ed200 | ||
|
|
f6d511ac1a | ||
|
|
bc23799c71 | ||
|
|
59e943b3c1 | ||
|
|
c8648fbfb8 | ||
|
|
96e7944fa8 | ||
|
|
79001fc361 | ||
|
|
2310b791f9 | ||
|
|
d814d40330 | ||
|
|
b6e098d1c2 | ||
|
|
db56748d88 | ||
|
|
68fb995c63 | ||
|
|
4420f11d9d | ||
|
|
75836affbe | ||
|
|
b284cc54df | ||
|
|
547e089185 | ||
|
|
fe2e0c44c8 | ||
|
|
30bd92c851 | ||
|
|
78afbd4292 | ||
|
|
f3a90d6994 | ||
|
|
44506ce15f | ||
|
|
5e92fa3404 | ||
|
|
1c36e2f586 | ||
|
|
16dd90ac78 | ||
|
|
7d9d299d5a | ||
|
|
0490ca67d1 | ||
|
|
e7dc96397c | ||
|
|
9bfdff0be1 | ||
|
|
143d9492b2 | ||
|
|
8e1a73dd0f | ||
|
|
8878eccb7b | ||
|
|
37eae7fb8a | ||
|
|
dd16b7cac3 | ||
|
|
68986e9143 | ||
|
|
62c1b542ed | ||
|
|
ee265394a6 | ||
|
|
9297a9cbb4 | ||
|
|
2118ab2503 | ||
|
|
2fff065b2c | ||
|
|
ed9abe3fa2 | ||
|
|
f5ea7d3c9c | ||
|
|
148a7ddda9 | ||
|
|
2f0920e4fb | ||
|
|
2e5b1e76ef | ||
|
|
db8510f110 | ||
|
|
e49278cc7d | ||
|
|
50f6790a27 | ||
|
|
a5aa111893 | ||
|
|
119fb08198 | ||
|
|
11ecc2c171 | ||
|
|
07f073361f | ||
|
|
5410700708 | ||
|
|
131af1fece | ||
|
|
a9a3e24bde | ||
|
|
39de557c4c | ||
|
|
4742899369 | ||
|
|
f3511d615e | ||
|
|
8f8772093d | ||
|
|
210bbc53a4 | ||
|
|
ce0537ef7f | ||
|
|
73cd902857 | ||
|
|
5d4514652d | ||
|
|
c07e651013 | ||
|
|
bc51bd93f4 | ||
|
|
72ce9ec321 | ||
|
|
a5d5f3f727 | ||
|
|
5be6f8ff36 | ||
|
|
28ef564974 | ||
|
|
aae9697d9a | ||
|
|
af3d9d8245 | ||
|
|
640729f312 | ||
|
|
de9d19d6f4 | ||
|
|
e64803e701 | ||
|
|
37bb626dd2 | ||
|
|
21273de6a1 | ||
|
|
fe271749c2 | ||
|
|
af0253b2eb | ||
|
|
986bcfef21 | ||
|
|
96f19c7205 | ||
|
|
cdc2df012c | ||
|
|
8dd790e745 | ||
|
|
e90e94b667 | ||
|
|
52f40b3370 | ||
|
|
8ed75217e1 | ||
|
|
1e92417804 | ||
|
|
be9cdf51d9 | ||
|
|
0e1a3c0665 | ||
|
|
0f7a4b1d6f | ||
|
|
acfee385fb | ||
|
|
96657841c8 | ||
|
|
a4dec0b6d2 | ||
|
|
06d3d8b827 | ||
|
|
0877ea07b3 | ||
|
|
31b89f602a | ||
|
|
1ffccfc91c | ||
|
|
81324806d5 | ||
|
|
a43f99a71c | ||
|
|
1347c3191f | ||
|
|
4e8e04fe66 | ||
|
|
9b8c64c8b6 | ||
|
|
a943b207ba | ||
|
|
23809bff64 | ||
|
|
a4f7828363 | ||
|
|
2598770b49 | ||
|
|
b77df372d6 | ||
|
|
8f774e9c53 | ||
|
|
47d9403e3a | ||
|
|
4d19092722 | ||
|
|
f2a38677fc | ||
|
|
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 | ||
|
|
af3ea5a321 | ||
|
|
c09b7b5d6d | ||
|
|
710454119f | ||
|
|
25e6d694e1 | ||
|
|
19a20b3b13 | ||
|
|
bd5b70c3cd | ||
|
|
ec5439e4d4 | ||
|
|
fd509e188a | ||
|
|
a5a839e72a | ||
|
|
3b53952dbe | ||
|
|
e502202de7 | ||
|
|
2479ce9123 | ||
|
|
d3772d4abd | ||
|
|
e9f36a7e45 | ||
|
|
f036bf9353 | ||
|
|
f4679cc870 | ||
|
|
7b116b0207 | ||
|
|
ffb19381f1 | ||
|
|
1525cbfb93 | ||
|
|
b83059c828 | ||
|
|
c7226ec28f | ||
|
|
c95c8a04ef | ||
|
|
d2d28fd419 | ||
|
|
67007aed40 | ||
|
|
df1c3dfb67 | ||
|
|
689484216d | ||
|
|
51c6029fe5 | ||
|
|
0eee544d17 | ||
|
|
21cca21124 | ||
|
|
f837451633 | ||
|
|
cce4a569e4 | ||
|
|
4feea9d7ec | ||
|
|
7aff588bf0 | ||
|
|
41a046a69d | ||
|
|
e548bd5312 | ||
|
|
88098283c7 | ||
|
|
6c6ed29329 | ||
|
|
c4f4e492e5 | ||
|
|
c286e2c434 |
45
.coveragerc
45
.coveragerc
@@ -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
|
||||
|
||||
@@ -170,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
|
||||
|
||||
@@ -187,6 +196,9 @@ omit =
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/toon.py
|
||||
homeassistant/components/*/toon.py
|
||||
|
||||
homeassistant/components/tradfri.py
|
||||
homeassistant/components/*/tradfri.py
|
||||
|
||||
@@ -217,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
|
||||
@@ -266,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
|
||||
@@ -294,6 +309,7 @@ omit =
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/hitron_coda.py
|
||||
homeassistant/components/device_tracker/huawei_router.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/keenetic_ndms2.py
|
||||
@@ -310,6 +326,7 @@ omit =
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tado.py
|
||||
homeassistant/components/device_tracker/tile.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
@@ -317,6 +334,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
|
||||
@@ -404,8 +422,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/clicksendaudio.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
@@ -427,6 +446,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
|
||||
@@ -436,8 +456,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
|
||||
@@ -454,7 +476,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
|
||||
@@ -462,6 +483,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
|
||||
@@ -489,17 +511,19 @@ 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/lacrosse.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
|
||||
@@ -507,6 +531,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
|
||||
@@ -523,12 +548,14 @@ omit =
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
homeassistant/components/sensor/pushbullet.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/pyload.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
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
|
||||
@@ -542,6 +569,7 @@ 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
|
||||
@@ -549,12 +577,15 @@ omit =
|
||||
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/viaggiatreno.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
|
||||
@@ -566,6 +597,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
|
||||
@@ -578,16 +610,19 @@ 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/vacuum/roomba.py
|
||||
homeassistant/components/weather/bom.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
|
||||
|
||||
36
CODEOWNERS
36
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,10 +39,37 @@ 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/media_player/kodi.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/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
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,10 +11,8 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
#ENV INSTALL_FFMPEG no
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_COAP no
|
||||
#ENV INSTALL_SSOCR no
|
||||
|
||||
|
||||
VOLUME /config
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
@@ -26,10 +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 |
@@ -19,15 +19,13 @@
|
||||
#
|
||||
import sys
|
||||
import os
|
||||
from os.path import relpath
|
||||
import inspect
|
||||
from homeassistant.const import (__version__, __short_version__, PROJECT_NAME,
|
||||
PROJECT_LONG_DESCRIPTION,
|
||||
PROJECT_COPYRIGHT, PROJECT_AUTHOR,
|
||||
PROJECT_GITHUB_USERNAME,
|
||||
PROJECT_GITHUB_REPOSITORY,
|
||||
GITHUB_PATH, GITHUB_URL)
|
||||
|
||||
from homeassistant.const import __version__, __short_version__
|
||||
from setup import (
|
||||
PROJECT_NAME, PROJECT_LONG_DESCRIPTION, PROJECT_COPYRIGHT, PROJECT_AUTHOR,
|
||||
PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY, GITHUB_PATH,
|
||||
GITHUB_URL)
|
||||
|
||||
sys.path.insert(0, os.path.abspath('_ext'))
|
||||
sys.path.insert(0, os.path.abspath('../homeassistant'))
|
||||
@@ -87,9 +85,7 @@ edit_on_github_src_path = 'docs/source/'
|
||||
|
||||
|
||||
def linkcode_resolve(domain, info):
|
||||
"""
|
||||
Determine the URL corresponding to Python object
|
||||
"""
|
||||
"""Determine the URL corresponding to Python object."""
|
||||
if domain != 'py':
|
||||
return None
|
||||
modname = info['module']
|
||||
|
||||
@@ -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
|
||||
@@ -32,8 +30,8 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
DATA_LOGGING = 'logging'
|
||||
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction',
|
||||
'frontend', 'history'))
|
||||
'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger',
|
||||
'introduction', 'frontend', 'history'))
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
@@ -90,7 +88,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
if sys.version_info[:2] < (3, 5):
|
||||
_LOGGER.warning(
|
||||
'Python 3.4 support has been deprecated and will be removed in '
|
||||
'the begining of 2018. Please upgrade Python or your operating '
|
||||
'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/'
|
||||
)
|
||||
|
||||
@@ -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.9']
|
||||
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.2']
|
||||
|
||||
_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
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.21']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.22']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -34,10 +34,8 @@ def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
||||
return
|
||||
|
||||
devices = [SpcAlarm(hass=hass,
|
||||
area_id=area['id'],
|
||||
name=area['name'],
|
||||
state=_get_alarm_state(area['mode']))
|
||||
api = hass.data[DATA_API]
|
||||
devices = [SpcAlarm(api, area)
|
||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
|
||||
async_add_devices(devices)
|
||||
@@ -46,21 +44,29 @@ def async_setup_platform(hass, config, async_add_devices,
|
||||
class SpcAlarm(alarm.AlarmControlPanel):
|
||||
"""Represents the SPC alarm panel."""
|
||||
|
||||
def __init__(self, hass, area_id, name, state):
|
||||
def __init__(self, api, area):
|
||||
"""Initialize the SPC alarm panel."""
|
||||
self._hass = hass
|
||||
self._area_id = area_id
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._api = hass.data[DATA_API]
|
||||
|
||||
hass.data[DATA_REGISTRY].register_alarm_device(area_id, self)
|
||||
self._area_id = area['id']
|
||||
self._name = area['name']
|
||||
self._state = _get_alarm_state(area['mode'])
|
||||
if self._state == STATE_ALARM_DISARMED:
|
||||
self._changed_by = area.get('last_unset_user_name', 'unknown')
|
||||
else:
|
||||
self._changed_by = area.get('last_set_user_name', 'unknown')
|
||||
self._api = api
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state):
|
||||
def async_added_to_hass(self):
|
||||
"""Calbback for init handlers."""
|
||||
self.hass.data[DATA_REGISTRY].register_alarm_device(
|
||||
self._area_id, self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state, extra):
|
||||
"""Update the alarm panel with a new state."""
|
||||
self._state = state
|
||||
yield from self.async_update_ha_state()
|
||||
self._changed_by = extra.get('changed_by', 'unknown')
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -72,6 +78,11 @@ class SpcAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Return the user the last change was triggered by."""
|
||||
return self._changed_by
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
|
||||
@@ -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.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -15,4 +15,6 @@ ATTR_STREAM_URL = 'streamUrl'
|
||||
ATTR_MAIN_TEXT = 'mainText'
|
||||
ATTR_REDIRECTION_URL = 'redirectionURL'
|
||||
|
||||
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
|
||||
@@ -3,6 +3,7 @@ Support for Alexa skill service end point.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import enum
|
||||
@@ -13,7 +14,7 @@ from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.components import http
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SYN_RESOLUTION_MATCH
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
|
||||
@@ -123,6 +124,43 @@ class AlexaIntentsView(http.HomeAssistantView):
|
||||
return self.json(alexa_response)
|
||||
|
||||
|
||||
def resolve_slot_synonyms(key, request):
|
||||
"""Check slot request for synonym resolutions."""
|
||||
# Default to the spoken slot value if more than one or none are found. For
|
||||
# reference to the request object structure, see the Alexa docs:
|
||||
# https://tinyurl.com/ybvm7jhs
|
||||
resolved_value = request['value']
|
||||
|
||||
if ('resolutions' in request and
|
||||
'resolutionsPerAuthority' in request['resolutions'] and
|
||||
len(request['resolutions']['resolutionsPerAuthority']) >= 1):
|
||||
|
||||
# Extract all of the possible values from each authority with a
|
||||
# successful match
|
||||
possible_values = []
|
||||
|
||||
for entry in request['resolutions']['resolutionsPerAuthority']:
|
||||
if entry['status']['code'] != SYN_RESOLUTION_MATCH:
|
||||
continue
|
||||
|
||||
possible_values.extend([item['value']['name']
|
||||
for item
|
||||
in entry['values']])
|
||||
|
||||
# If there is only one match use the resolved value, otherwise the
|
||||
# resolution cannot be determined, so use the spoken slot value
|
||||
if len(possible_values) == 1:
|
||||
resolved_value = possible_values[0]
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
'Found multiple synonym resolutions for slot value: {%s: %s}',
|
||||
key,
|
||||
request['value']
|
||||
)
|
||||
|
||||
return resolved_value
|
||||
|
||||
|
||||
class AlexaResponse(object):
|
||||
"""Help generating the response for Alexa."""
|
||||
|
||||
@@ -135,12 +173,17 @@ class AlexaResponse(object):
|
||||
self.session_attributes = {}
|
||||
self.should_end_session = True
|
||||
self.variables = {}
|
||||
|
||||
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
||||
if intent_info is not None:
|
||||
for key, value in intent_info.get('slots', {}).items():
|
||||
if 'value' in value:
|
||||
underscored_key = key.replace('.', '_')
|
||||
self.variables[underscored_key] = value['value']
|
||||
# Only include slots with values
|
||||
if 'value' not in value:
|
||||
continue
|
||||
|
||||
_key = key.replace('.', '_')
|
||||
|
||||
self.variables[_key] = resolve_slot_synonyms(key, value)
|
||||
|
||||
def add_card(self, card_type, title, content):
|
||||
"""Add a card to the response."""
|
||||
|
||||
@@ -1,177 +1,643 @@
|
||||
"""Support for alexa Smart Home Skill API."""
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import math
|
||||
from uuid import uuid4
|
||||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
from homeassistant.components import switch, light
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_LOCK,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET)
|
||||
from homeassistant.components import (
|
||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
||||
media_player, scene, script, switch)
|
||||
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_ENDPOINT = 'endpoint'
|
||||
API_EVENT = 'event'
|
||||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
|
||||
ATTR_ALEXA_DESCRIPTION = 'alexa_description'
|
||||
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories'
|
||||
ATTR_ALEXA_HIDDEN = 'alexa_hidden'
|
||||
ATTR_ALEXA_NAME = 'alexa_name'
|
||||
|
||||
|
||||
MAPPING_COMPONENT = {
|
||||
switch.DOMAIN: ['SWITCH', ('turnOff', 'turnOn'), None],
|
||||
light.DOMAIN: [
|
||||
'LIGHT', ('turnOff', 'turnOn'), {
|
||||
light.SUPPORT_BRIGHTNESS: 'setPercentage'
|
||||
alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
cover.DOMAIN: [
|
||||
'DOOR', ('Alexa.PowerController',), {
|
||||
cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController',
|
||||
}
|
||||
],
|
||||
fan.DOMAIN: [
|
||||
'OTHER', ('Alexa.PowerController',), {
|
||||
fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController',
|
||||
}
|
||||
],
|
||||
group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
light.DOMAIN: [
|
||||
'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',
|
||||
}
|
||||
],
|
||||
lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None],
|
||||
media_player.DOMAIN: [
|
||||
'TV', ('Alexa.PowerController',), {
|
||||
media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker',
|
||||
media_player.SUPPORT_PLAY: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_STOP: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController',
|
||||
}
|
||||
],
|
||||
scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None],
|
||||
script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
|
||||
}
|
||||
|
||||
|
||||
Config = namedtuple('AlexaConfig', 'filter')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, message):
|
||||
def async_handle_message(hass, config, message):
|
||||
"""Handle incoming API messages."""
|
||||
assert int(message[ATTR_HEADER][ATTR_PAYLOAD_VERSION]) == 2
|
||||
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 = HANDLERS.get(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))
|
||||
return (yield from funct_ref(hass, config, message))
|
||||
|
||||
|
||||
def api_message(name, namespace, payload=None):
|
||||
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: str(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'):
|
||||
# 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('DiscoverAppliancesRequest')
|
||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||
@asyncio.coroutine
|
||||
def async_api_discovery(hass, request):
|
||||
def async_api_discovery(hass, config, request):
|
||||
"""Create a API formatted discovery response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
discovered_appliances = []
|
||||
discovery_endpoints = []
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
if not config.filter(entity.entity_id):
|
||||
_LOGGER.debug("Not exposing %s because filtered by config",
|
||||
entity.entity_id)
|
||||
continue
|
||||
|
||||
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
|
||||
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
|
||||
entity.entity_id)
|
||||
continue
|
||||
|
||||
class_data = MAPPING_COMPONENT.get(entity.domain)
|
||||
|
||||
if not class_data:
|
||||
continue
|
||||
|
||||
appliance = {
|
||||
'actions': [],
|
||||
'applianceTypes': [class_data[0]],
|
||||
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name)
|
||||
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
|
||||
entity.entity_id)
|
||||
|
||||
# Required description as per Amazon Scene docs
|
||||
if entity.domain == scene.DOMAIN:
|
||||
scene_fmt = '%s (Scene connected via Home Assistant)'
|
||||
description = scene_fmt.format(description)
|
||||
|
||||
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES
|
||||
display_categories = entity.attributes.get(cat_key, class_data[0])
|
||||
|
||||
endpoint = {
|
||||
'displayCategories': [display_categories],
|
||||
'additionalApplianceDetails': {},
|
||||
'applianceId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyDescription': '',
|
||||
'friendlyName': entity.name,
|
||||
'isReachable': True,
|
||||
'manufacturerName': 'Unknown',
|
||||
'modelName': 'Unknown',
|
||||
'version': 'Unknown',
|
||||
'endpointId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyName': friendly_name,
|
||||
'description': description,
|
||||
'manufacturerName': 'Home Assistant',
|
||||
}
|
||||
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):
|
||||
"""Decorator for extract entity object from request."""
|
||||
@asyncio.coroutine
|
||||
def async_api_entity_wrapper(hass, request):
|
||||
def async_api_entity_wrapper(hass, config, 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 (yield from funct(hass, config, request, entity))
|
||||
|
||||
return async_api_entity_wrapper
|
||||
|
||||
|
||||
@HANDLERS.register('TurnOnRequest')
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_on(hass, request, entity):
|
||||
def async_api_turn_on(hass, config, request, entity):
|
||||
"""Process a turn on request."""
|
||||
domain = entity.domain
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_off(hass, config, request, entity):
|
||||
"""Process a turn off request."""
|
||||
domain = entity.domain
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_brightness(hass, config, 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, config, 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, config, 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_RGB_COLOR: rgb,
|
||||
}, blocking=True)
|
||||
else:
|
||||
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(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_color_temperature(hass, config, 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, config, 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, config, 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)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_activate(hass, config, request, entity):
|
||||
"""Process a activate request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message('TurnOnConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register('TurnOffRequest')
|
||||
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_off(hass, request, entity):
|
||||
"""Process a turn off request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_OFF, {
|
||||
def async_api_set_percentage(hass, config, request, entity):
|
||||
"""Process a set percentage request."""
|
||||
percentage = int(request[API_PAYLOAD]['percentage'])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = "off"
|
||||
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
elif percentage <= 100:
|
||||
speed = "high"
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = percentage
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_percentage(hass, config, request, entity):
|
||||
"""Process a adjust percentage request."""
|
||||
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = entity.attributes.get(fan.ATTR_SPEED)
|
||||
|
||||
if speed == "off":
|
||||
current = 0
|
||||
elif speed == "low":
|
||||
current = 33
|
||||
elif speed == "medium":
|
||||
current = 66
|
||||
elif speed == "high":
|
||||
current = 100
|
||||
|
||||
# set percentage
|
||||
percentage = max(0, percentage_delta + current)
|
||||
speed = "off"
|
||||
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
elif percentage <= 100:
|
||||
speed = "high"
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
|
||||
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||
|
||||
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.LockController', 'Lock'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_lock(hass, config, request, entity):
|
||||
"""Process a lock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message('TurnOffConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register('SetPercentageRequest')
|
||||
# Not supported by Alexa yet
|
||||
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
|
||||
@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']
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS: brightness,
|
||||
}, blocking=True)
|
||||
else:
|
||||
return api_error(request)
|
||||
def async_api_unlock(hass, config, request, entity):
|
||||
"""Process a unlock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(
|
||||
'SetPercentageConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_volume(hass, config, request, entity):
|
||||
"""Process a set volume request."""
|
||||
volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_volume(hass, config, request, entity):
|
||||
"""Process a adjust volume request."""
|
||||
volume_delta = int(request[API_PAYLOAD]['volume'])
|
||||
|
||||
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
||||
|
||||
# read current state
|
||||
try:
|
||||
current = math.floor(int(current_level * 100))
|
||||
except ZeroDivisionError:
|
||||
current = 0
|
||||
|
||||
volume = float(max(0, volume_delta + current) / 100)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_mute(hass, config, request, entity):
|
||||
"""Process a set mute request."""
|
||||
mute = bool(request[API_PAYLOAD]['mute'])
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_MUTE,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_play(hass, config, request, entity):
|
||||
"""Process a play request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_pause(hass, config, request, entity):
|
||||
"""Process a pause request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_stop(hass, config, request, entity):
|
||||
"""Process a stop request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_next(hass, config, request, entity):
|
||||
"""Process a next request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_previous(hass, config, request, entity):
|
||||
"""Process a previous request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, 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.5']
|
||||
REQUIREMENTS = ['pyatv==0.3.8']
|
||||
|
||||
_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.6']
|
||||
REQUIREMENTS = ['pyarlo==0.1.0']
|
||||
|
||||
_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,9 @@ 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
|
||||
|
||||
if event_types:
|
||||
hass.add_job(device.start)
|
||||
return True
|
||||
|
||||
|
||||
@@ -287,25 +293,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 +318,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 +333,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'}, ]
|
||||
|
||||
@@ -30,6 +30,7 @@ DEVICE_CLASSES = [
|
||||
'moving', # On means moving, Off means stopped
|
||||
'occupancy', # On means occupied, Off means not occupied
|
||||
'opening', # Door, window, etc.
|
||||
'plug', # On means plugged in, Off means unplugged
|
||||
'power', # Power, over-current, etc
|
||||
'safety', # Generic on=unsafe, off=safe
|
||||
'smoke', # Smoke detector
|
||||
|
||||
@@ -7,25 +7,32 @@ https://home-assistant.io/components/binary_sensor.aurora/
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import USER_AGENT
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor \
|
||||
import (BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
CONF_THRESHOLD = "forecast_threshold"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" \
|
||||
"Administration"
|
||||
CONF_THRESHOLD = 'forecast_threshold'
|
||||
|
||||
DEFAULT_DEVICE_CLASS = 'visible'
|
||||
DEFAULT_NAME = 'Aurora Visibility'
|
||||
DEFAULT_DEVICE_CLASS = "visible"
|
||||
DEFAULT_THRESHOLD = 75
|
||||
|
||||
HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
|
||||
URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
|
||||
@@ -43,10 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
try:
|
||||
aurora_data = AuroraData(
|
||||
hass.config.latitude,
|
||||
hass.config.longitude,
|
||||
threshold
|
||||
)
|
||||
hass.config.latitude, hass.config.longitude, threshold)
|
||||
aurora_data.update()
|
||||
except requests.exceptions.HTTPError as error:
|
||||
_LOGGER.error(
|
||||
@@ -85,9 +89,9 @@ class AuroraSensor(BinarySensorDevice):
|
||||
attrs = {}
|
||||
|
||||
if self.aurora_data:
|
||||
attrs["visibility_level"] = self.aurora_data.visibility_level
|
||||
attrs["message"] = self.aurora_data.is_visible_text
|
||||
|
||||
attrs['visibility_level'] = self.aurora_data.visibility_level
|
||||
attrs['message'] = self.aurora_data.is_visible_text
|
||||
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
@@ -104,10 +108,7 @@ class AuroraData(object):
|
||||
self.longitude = longitude
|
||||
self.number_of_latitude_intervals = 513
|
||||
self.number_of_longitude_intervals = 1024
|
||||
self.api_url = \
|
||||
"http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
|
||||
self.headers = {"User-Agent": "Home Assistant Aurora Tracker v.0.1.0"}
|
||||
|
||||
self.headers = {USER_AGENT: HA_USER_AGENT}
|
||||
self.threshold = int(threshold)
|
||||
self.is_visible = None
|
||||
self.is_visible_text = None
|
||||
@@ -132,14 +133,14 @@ class AuroraData(object):
|
||||
|
||||
def get_aurora_forecast(self):
|
||||
"""Get forecast data and parse for given long/lat."""
|
||||
raw_data = requests.get(self.api_url, headers=self.headers).text
|
||||
raw_data = requests.get(URL, headers=self.headers, timeout=5).text
|
||||
forecast_table = [
|
||||
row.strip(" ").split(" ")
|
||||
for row in raw_data.split("\n")
|
||||
if not row.startswith("#")
|
||||
]
|
||||
|
||||
# convert lat and long for data points in table
|
||||
# Convert lat and long for data points in table
|
||||
converted_latitude = round((self.latitude / 180)
|
||||
* self.number_of_latitude_intervals)
|
||||
converted_longitude = round((self.longitude / 360)
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -59,6 +59,8 @@ class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice):
|
||||
"""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):
|
||||
|
||||
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)
|
||||
@@ -28,20 +28,20 @@ SCAN_INTERVAL = timedelta(seconds=5)
|
||||
# Sensor types: Name, category, device_class
|
||||
SENSOR_TYPES = {
|
||||
'ding': ['Ding', ['doorbell'], 'occupancy'],
|
||||
'motion': ['Motion', ['doorbell'], 'motion'],
|
||||
'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
|
||||
@@ -67,7 +67,7 @@ class SpcBinarySensor(BinarySensorDevice):
|
||||
spc_registry.register_sensor_device(zone_id, self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state):
|
||||
def async_update_from_spc(self, state, extra):
|
||||
"""Update the state of the device."""
|
||||
self._state = state
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
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
|
||||
|
||||
@@ -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]
|
||||
|
||||
103
homeassistant/components/binary_sensor/vultr.py
Normal file
103
homeassistant/components/binary_sensor/vultr.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Support for monitoring the state of Vultr subscriptions (VPS).
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.vultr/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.vultr import (
|
||||
CONF_SUBSCRIPTION, ATTR_AUTO_BACKUPS, ATTR_ALLOWED_BANDWIDTH,
|
||||
ATTR_CREATED_AT, ATTR_SUBSCRIPTION_ID, ATTR_SUBSCRIPTION_NAME,
|
||||
ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, ATTR_DISK,
|
||||
ATTR_COST_PER_MONTH, ATTR_OS, ATTR_REGION, ATTR_VCPUS, DATA_VULTR)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DEVICE_CLASS = 'power'
|
||||
DEFAULT_NAME = 'Vultr {}'
|
||||
DEPENDENCIES = ['vultr']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SUBSCRIPTION): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Vultr subscription (server) sensor."""
|
||||
vultr = hass.data[DATA_VULTR]
|
||||
|
||||
subscription = config.get(CONF_SUBSCRIPTION)
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
if subscription not in vultr.data:
|
||||
_LOGGER.error("Subscription %s not found", subscription)
|
||||
return False
|
||||
|
||||
add_devices([VultrBinarySensor(vultr, subscription, name)], True)
|
||||
|
||||
|
||||
class VultrBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Vultr subscription sensor."""
|
||||
|
||||
def __init__(self, vultr, subscription, name):
|
||||
"""Initialize a new Vultr sensor."""
|
||||
self._vultr = vultr
|
||||
self._name = name
|
||||
|
||||
self.subscription = subscription
|
||||
self.data = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
try:
|
||||
return self._name.format(self.data['label'])
|
||||
except (KeyError, TypeError):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of this server."""
|
||||
return 'mdi:server' if self.is_on else 'mdi:server-off'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.data['power_status'] == 'running'
|
||||
|
||||
@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 Vultr subscription."""
|
||||
return {
|
||||
ATTR_ALLOWED_BANDWIDTH: self.data.get('allowed_bandwidth_gb'),
|
||||
ATTR_AUTO_BACKUPS: self.data.get('auto_backups'),
|
||||
ATTR_COST_PER_MONTH: self.data.get('cost_per_month'),
|
||||
ATTR_CREATED_AT: self.data.get('date_created'),
|
||||
ATTR_DISK: self.data.get('disk'),
|
||||
ATTR_IPV4_ADDRESS: self.data.get('main_ip'),
|
||||
ATTR_IPV6_ADDRESS: self.data.get('v6_main_ip'),
|
||||
ATTR_MEMORY: self.data.get('ram'),
|
||||
ATTR_OS: self.data.get('os'),
|
||||
ATTR_REGION: self.data.get('location'),
|
||||
ATTR_SUBSCRIPTION_ID: self.data.get('SUBID'),
|
||||
ATTR_SUBSCRIPTION_NAME: self.data.get('label'),
|
||||
ATTR_VCPUS: self.data.get('vcpu_count')
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Update state of sensor."""
|
||||
self._vultr.update()
|
||||
self.data = self._vultr.data[self.subscription]
|
||||
@@ -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,11 +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(),
|
||||
'pairing_mode': self.wink.pairing_mode()
|
||||
}
|
||||
_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):
|
||||
@@ -148,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):
|
||||
@@ -167,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
|
||||
|
||||
@@ -4,16 +4,17 @@ Support for BloomSky weather station.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/bloomsky/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import AUTHORIZATION
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -68,7 +69,7 @@ class BloomSky(object):
|
||||
"""Use the API to retrieve a list of devices."""
|
||||
_LOGGER.debug("Fetching BloomSky update")
|
||||
response = requests.get(
|
||||
self.API_URL, headers={"Authorization": self._api_key}, timeout=10)
|
||||
self.API_URL, headers={AUTHORIZATION: self._api_key}, timeout=10)
|
||||
if response.status_code == 401:
|
||||
raise RuntimeError("Invalid API_KEY")
|
||||
elif response.status_code != 200:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
"""
|
||||
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
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=90)
|
||||
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
|
||||
ATTR_BRIGHTNESS = 'brightness'
|
||||
ATTR_FLIPPED = 'flipped'
|
||||
ATTR_MIRRORED = 'mirrored'
|
||||
ATTR_MOTION_SENSITIVITY = 'motion_detection_sensitivity'
|
||||
ATTR_POWER_SAVE_MODE = 'power_save_mode'
|
||||
ATTR_MOTION = 'motion_detection_sensitivity'
|
||||
ATTR_POWERSAVE = 'power_save_mode'
|
||||
ATTR_SIGNAL_STRENGTH = 'signal_strength'
|
||||
ATTR_UNSEEN_VIDEOS = 'unseen_videos'
|
||||
ATTR_LAST_REFRESH = 'last_refresh'
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
|
||||
POWERSAVE_MODE_MAPPING = {
|
||||
1: 'best_battery_life',
|
||||
@@ -40,7 +44,8 @@ POWERSAVE_MODE_MAPPING = {
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS):
|
||||
cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -69,6 +74,9 @@ class ArloCam(Camera):
|
||||
self._motion_status = False
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._last_refresh = None
|
||||
self._camera.base_station.refresh_rate = SCAN_INTERVAL.total_seconds()
|
||||
self.attrs = {}
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
@@ -100,32 +108,27 @@ class ArloCam(Camera):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL:
|
||||
self._camera.get_battery_level,
|
||||
ATTR_BRIGHTNESS:
|
||||
self._camera.get_brightness,
|
||||
ATTR_FLIPPED:
|
||||
self._camera.get_flip_state,
|
||||
ATTR_MIRRORED:
|
||||
self._camera.get_mirror_state,
|
||||
ATTR_MOTION_SENSITIVITY:
|
||||
self._camera.get_motion_detection_sensitivity,
|
||||
ATTR_POWER_SAVE_MODE:
|
||||
POWERSAVE_MODE_MAPPING[self._camera.get_powersave_mode],
|
||||
ATTR_SIGNAL_STRENGTH:
|
||||
self._camera.get_signal_strength,
|
||||
ATTR_UNSEEN_VIDEOS:
|
||||
self._camera.unseen_videos
|
||||
name: value for name, value in (
|
||||
(ATTR_BATTERY_LEVEL, self._camera.battery_level),
|
||||
(ATTR_BRIGHTNESS, self._camera.brightness),
|
||||
(ATTR_FLIPPED, self._camera.flip_state),
|
||||
(ATTR_MIRRORED, self._camera.mirror_state),
|
||||
(ATTR_MOTION, self._camera.motion_detection_sensitivity),
|
||||
(ATTR_POWERSAVE, POWERSAVE_MODE_MAPPING.get(
|
||||
self._camera.powersave_mode)),
|
||||
(ATTR_SIGNAL_STRENGTH, self._camera.signal_strength),
|
||||
(ATTR_UNSEEN_VIDEOS, self._camera.unseen_videos),
|
||||
) if value is not None
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -135,7 +138,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):
|
||||
@@ -143,7 +146,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
|
||||
@@ -160,3 +163,7 @@ 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._camera.update()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
143
homeassistant/components/camera/ring.py
Normal file
143
homeassistant/components/camera/ring.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
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 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']
|
||||
|
||||
FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
|
||||
|
||||
_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._utcnow = dt_util.utcnow()
|
||||
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|
||||
|
||||
@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."""
|
||||
_LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url")
|
||||
|
||||
self._camera.update()
|
||||
self._utcnow = dt_util.utcnow()
|
||||
|
||||
last_recording_id = self._camera.last_recording_id
|
||||
|
||||
if self._last_video_id != last_recording_id or \
|
||||
self._utcnow >= self._expires_at:
|
||||
|
||||
_LOGGER.info("Ring DoorBell properties refreshed")
|
||||
|
||||
# update attributes if new video or if URL has expired
|
||||
self._last_video_id = self._camera.last_recording_id
|
||||
self._video_url = self._camera.recording_url(self._last_video_id)
|
||||
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|
||||
@@ -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
|
||||
@@ -16,11 +16,11 @@ from homeassistant.const import (
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_create_clientsession,
|
||||
async_aiohttp_proxy_web)
|
||||
async_aiohttp_proxy_web,
|
||||
async_get_clientsession)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['py-synology==0.1.1']
|
||||
REQUIREMENTS = ['py-synology==0.1.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,13 +58,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
return False
|
||||
|
||||
cameras = surveillance.get_all_cameras()
|
||||
websession = async_create_clientsession(hass, verify_ssl)
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
for camera in cameras:
|
||||
if not config.get(CONF_WHITELIST):
|
||||
device = SynologyCamera(websession, surveillance, camera.camera_id)
|
||||
device = SynologyCamera(surveillance, camera.camera_id, verify_ssl)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
@@ -73,12 +72,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
||||
def __init__(self, websession, surveillance, camera_id):
|
||||
def __init__(self, surveillance, camera_id, verify_ssl):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self._websession = websession
|
||||
self._surveillance = surveillance
|
||||
self._camera_id = camera_id
|
||||
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
|
||||
@@ -91,7 +90,9 @@ class SynologyCamera(Camera):
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
streaming_url = self._camera.video_stream_url
|
||||
stream_coro = self._websession.get(streaming_url)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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()
|
||||
@@ -9,12 +9,12 @@ from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import functools as ft
|
||||
from numbers import Number
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.temperature import display_temp as show_temp
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -22,7 +22,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
TEMP_CELSIUS)
|
||||
TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS)
|
||||
|
||||
DOMAIN = 'climate'
|
||||
|
||||
@@ -71,11 +71,6 @@ ATTR_OPERATION_LIST = 'operation_list'
|
||||
ATTR_SWING_MODE = 'swing_mode'
|
||||
ATTR_SWING_LIST = 'swing_list'
|
||||
|
||||
# The degree of precision for each platform
|
||||
PRECISION_WHOLE = 1
|
||||
PRECISION_HALVES = 0.5
|
||||
PRECISION_TENTHS = 0.1
|
||||
|
||||
CONVERTIBLE_ATTRIBUTE = [
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
@@ -236,24 +231,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."""
|
||||
@@ -261,13 +238,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,
|
||||
@@ -281,10 +264,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,
|
||||
@@ -298,13 +287,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,
|
||||
@@ -316,6 +311,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():
|
||||
@@ -330,7 +326,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,
|
||||
@@ -344,10 +345,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,
|
||||
@@ -361,10 +367,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,
|
||||
@@ -378,10 +389,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,
|
||||
@@ -395,10 +411,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,
|
||||
@@ -430,12 +451,18 @@ class ClimateDevice(Entity):
|
||||
def state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {
|
||||
ATTR_CURRENT_TEMPERATURE:
|
||||
self._convert_for_display(self.current_temperature),
|
||||
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp),
|
||||
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
|
||||
ATTR_TEMPERATURE:
|
||||
self._convert_for_display(self.target_temperature),
|
||||
ATTR_CURRENT_TEMPERATURE: show_temp(
|
||||
self.hass, self.current_temperature, self.temperature_unit,
|
||||
self.precision),
|
||||
ATTR_MIN_TEMP: show_temp(
|
||||
self.hass, self.min_temp, self.temperature_unit,
|
||||
self.precision),
|
||||
ATTR_MAX_TEMP: show_temp(
|
||||
self.hass, self.max_temp, self.temperature_unit,
|
||||
self.precision),
|
||||
ATTR_TEMPERATURE: show_temp(
|
||||
self.hass, self.target_temperature, self.temperature_unit,
|
||||
self.precision),
|
||||
}
|
||||
|
||||
if self.target_temperature_step is not None:
|
||||
@@ -443,10 +470,12 @@ class ClimateDevice(Entity):
|
||||
|
||||
target_temp_high = self.target_temperature_high
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
|
||||
self.hass, self.target_temperature_high, self.temperature_unit,
|
||||
self.precision)
|
||||
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
||||
self.hass, self.target_temperature_low, self.temperature_unit,
|
||||
self.precision)
|
||||
|
||||
humidity = self.target_humidity
|
||||
if humidity is not None:
|
||||
@@ -707,24 +736,3 @@ class ClimateDevice(Entity):
|
||||
def max_humidity(self):
|
||||
"""Return the maximum humidity."""
|
||||
return 99
|
||||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if temp is None:
|
||||
return temp
|
||||
|
||||
# if the temperature is not a number this can cause issues
|
||||
# with polymer components, so bail early there.
|
||||
if not isinstance(temp, Number):
|
||||
raise TypeError("Temperature is not a number: %s" % temp)
|
||||
|
||||
if self.temperature_unit != self.unit_of_measurement:
|
||||
temp = convert_temperature(
|
||||
temp, self.temperature_unit, self.unit_of_measurement)
|
||||
# Round in the units appropriate
|
||||
if self.precision == PRECISION_HALVES:
|
||||
return round(temp * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(temp, 1)
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
|
||||
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)
|
||||
@@ -9,15 +9,12 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, PRECISION_HALVES,
|
||||
STATE_AUTO, STATE_ON, STATE_OFF,
|
||||
)
|
||||
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
|
||||
|
||||
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.5']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,15 +55,17 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, _mac, _name):
|
||||
"""Initialize the thermostat."""
|
||||
# we want to avoid name clash with this module..
|
||||
# We want to avoid name clash with this module.
|
||||
import eq3bt as eq3
|
||||
|
||||
self.modes = {eq3.Mode.Open: STATE_ON,
|
||||
eq3.Mode.Closed: STATE_OFF,
|
||||
eq3.Mode.Auto: STATE_AUTO,
|
||||
eq3.Mode.Manual: STATE_MANUAL,
|
||||
eq3.Mode.Boost: STATE_BOOST,
|
||||
eq3.Mode.Away: STATE_AWAY}
|
||||
self.modes = {
|
||||
eq3.Mode.Open: STATE_ON,
|
||||
eq3.Mode.Closed: STATE_OFF,
|
||||
eq3.Mode.Auto: STATE_AUTO,
|
||||
eq3.Mode.Manual: STATE_MANUAL,
|
||||
eq3.Mode.Boost: STATE_BOOST,
|
||||
eq3.Mode.Away: STATE_AWAY,
|
||||
}
|
||||
|
||||
self.reverse_modes = {v: k for k, v in self.modes.items()}
|
||||
|
||||
@@ -153,15 +152,19 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
dev_specific = {
|
||||
ATTR_STATE_AWAY_END: self._thermostat.away_end,
|
||||
ATTR_STATE_LOCKED: self._thermostat.locked,
|
||||
ATTR_STATE_LOW_BAT: self._thermostat.low_battery,
|
||||
ATTR_STATE_VALVE: self._thermostat.valve_state,
|
||||
ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open,
|
||||
ATTR_STATE_AWAY_END: self._thermostat.away_end,
|
||||
}
|
||||
|
||||
return dev_specific
|
||||
|
||||
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
|
||||
|
||||
@@ -156,6 +163,7 @@ class GenericThermostat(ClimateDevice):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_AUTO:
|
||||
self._enabled = True
|
||||
self._async_control_heating()
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
@@ -261,25 +269,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)
|
||||
|
||||
@@ -7,7 +7,6 @@ https://home-assistant.io/components/climate.homematic/
|
||||
import logging
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.util.temperature import convert
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
@@ -121,12 +120,12 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature - 4.5 means off."""
|
||||
return convert(4.5, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return 4.5
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature - 30.5 means on."""
|
||||
return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return 30.5
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,11 @@ from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
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_SETPOINT_SHIFT_STEP = 'setpoint_shift_step'
|
||||
CONF_SETPOINT_SHIFT_MAX = 'setpoint_shift_max'
|
||||
CONF_SETPOINT_SHIFT_MIN = 'setpoint_shift_min'
|
||||
CONF_TEMPERATURE_ADDRESS = 'temperature_address'
|
||||
CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address'
|
||||
CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
|
||||
@@ -26,13 +30,24 @@ CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address'
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address'
|
||||
|
||||
DEFAULT_NAME = 'KNX Climate'
|
||||
DEFAULT_SETPOINT_SHIFT_STEP = 0.5
|
||||
DEFAULT_SETPOINT_SHIFT_MAX = 6
|
||||
DEFAULT_SETPOINT_SHIFT_MIN = -6
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
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_SETPOINT_SHIFT_STEP,
|
||||
default=DEFAULT_SETPOINT_SHIFT_STEP): vol.All(
|
||||
float, vol.Range(min=0, max=2)),
|
||||
vol.Optional(CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX):
|
||||
vol.All(int, vol.Range(min=-32, max=0)),
|
||||
vol.Optional(CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN):
|
||||
vol.All(int, vol.Range(min=0, max=32)),
|
||||
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,
|
||||
@@ -73,6 +88,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
||||
def async_add_devices_config(hass, config, async_add_devices):
|
||||
"""Set up climate for KNX platform configured within plattform."""
|
||||
import xknx
|
||||
|
||||
climate = xknx.devices.Climate(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME),
|
||||
@@ -80,8 +96,16 @@ def async_add_devices_config(hass, config, async_add_devices):
|
||||
CONF_TEMPERATURE_ADDRESS),
|
||||
group_address_target_temperature=config.get(
|
||||
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),
|
||||
setpoint_shift_step=config.get(
|
||||
CONF_SETPOINT_SHIFT_STEP),
|
||||
setpoint_shift_max=config.get(
|
||||
CONF_SETPOINT_SHIFT_MAX),
|
||||
setpoint_shift_min=config.get(
|
||||
CONF_SETPOINT_SHIFT_MIN),
|
||||
group_address_operation_mode=config.get(
|
||||
CONF_OPERATION_MODE_ADDRESS),
|
||||
group_address_operation_mode_state=config.get(
|
||||
@@ -110,8 +134,6 @@ class KNXClimate(ClimateDevice):
|
||||
self.async_register_callbacks()
|
||||
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self._away = False # not yet supported
|
||||
self._is_fan_on = False # not yet supported
|
||||
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@@ -140,14 +162,27 @@ 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_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self.device.setpoint_shift_step
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.device.supports_target_temperature:
|
||||
return self.device.target_temperature
|
||||
return None
|
||||
return self.device.target_temperature.value
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self.device.target_temperature_min
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self.device.target_temperature_max
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
@@ -155,8 +190,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(temperature)
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
@@ -18,7 +18,8 @@ from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
|
||||
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN)
|
||||
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)
|
||||
@@ -57,7 +58,8 @@ CONF_SWING_MODE_LIST = 'swing_modes'
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_SEND_IF_OFF = 'send_if_off'
|
||||
|
||||
PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({
|
||||
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,
|
||||
|
||||
@@ -1,132 +1,102 @@
|
||||
# Describes the format for available climate services
|
||||
|
||||
set_aux_heat:
|
||||
description: Turn auxiliary 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()
|
||||
@@ -4,46 +4,51 @@ 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
|
||||
import logging
|
||||
|
||||
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, STATE_FAN_ONLY,
|
||||
ATTR_CURRENT_HUMIDITY, STATE_ECO, STATE_ELECTRIC,
|
||||
STATE_PERFORMANCE, STATE_HIGH_DEMAND,
|
||||
STATE_HEAT_PUMP, STATE_GAS)
|
||||
STATE_ECO, STATE_GAS, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ELECTRIC,
|
||||
STATE_FAN_ONLY, STATE_HEAT_PUMP, ATTR_TEMPERATURE, STATE_HIGH_DEMAND,
|
||||
STATE_PERFORMANCE, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_TARGET_TEMP_HIGH, ClimateDevice)
|
||||
from homeassistant.components.wink import DOMAIN, WinkDevice
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, STATE_ON,
|
||||
STATE_OFF, STATE_UNKNOWN)
|
||||
STATE_ON, STATE_OFF, TEMP_CELSIUS, STATE_UNKNOWN, PRECISION_TENTHS)
|
||||
from homeassistant.helpers.temperature import display_temp as show_temp
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ECO_TARGET = 'eco_target'
|
||||
ATTR_EXTERNAL_TEMPERATURE = 'external_temperature'
|
||||
ATTR_OCCUPIED = 'occupied'
|
||||
ATTR_RHEEM_TYPE = 'rheem_type'
|
||||
ATTR_SCHEDULE_ENABLED = 'schedule_enabled'
|
||||
ATTR_SMART_TEMPERATURE = 'smart_temperature'
|
||||
ATTR_TOTAL_CONSUMPTION = 'total_consumption'
|
||||
ATTR_VACATION_MODE = 'vacation_mode'
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
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()}
|
||||
HA_STATE_TO_WINK = {
|
||||
STATE_AUTO: 'auto',
|
||||
STATE_COOL: 'cool_only',
|
||||
STATE_ECO: 'eco',
|
||||
STATE_ELECTRIC: 'electric_only',
|
||||
STATE_FAN_ONLY: 'fan_only',
|
||||
STATE_GAS: 'gas',
|
||||
STATE_HEAT: 'heat_only',
|
||||
STATE_HEAT_PUMP: 'heat_pump',
|
||||
STATE_HIGH_DEMAND: 'high_demand',
|
||||
STATE_OFF: 'off',
|
||||
STATE_PERFORMANCE: 'performance',
|
||||
}
|
||||
|
||||
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
ATTR_SMART_TEMPERATURE = "smart_temperature"
|
||||
ATTR_ECO_TARGET = "eco_target"
|
||||
ATTR_OCCUPIED = "occupied"
|
||||
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -85,15 +90,18 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
target_temp_high = self.target_temperature_high
|
||||
target_temp_low = self.target_temperature_low
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
|
||||
self.hass, self.target_temperature_high, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
if target_temp_low is not None:
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
||||
self.hass, self.target_temperature_low, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
|
||||
if self.external_temperature:
|
||||
data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display(
|
||||
self.external_temperature)
|
||||
data[ATTR_EXTERNAL_TEMPERATURE] = show_temp(
|
||||
self.hass, self.external_temperature, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
|
||||
if self.smart_temperature:
|
||||
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
|
||||
@@ -139,7 +147,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
|
||||
@property
|
||||
def eco_target(self):
|
||||
"""Return status of eco target (Is the termostat in eco mode)."""
|
||||
"""Return status of eco target (Is the thermostat in eco mode)."""
|
||||
return self.wink.eco_target()
|
||||
|
||||
@property
|
||||
@@ -249,7 +257,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invaid operation mode mapping. " + mode + \
|
||||
error = "Invalid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
@@ -297,7 +305,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
minimum = 7 # Default minimum
|
||||
min_min = self.wink.min_min_set_point()
|
||||
min_max = self.wink.min_max_set_point()
|
||||
return_value = minimum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if min_min:
|
||||
return_value = min_min
|
||||
@@ -323,7 +330,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
maximum = 35 # Default maximum
|
||||
max_min = self.wink.max_min_set_point()
|
||||
max_max = self.wink.max_max_set_point()
|
||||
return_value = maximum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if max_min:
|
||||
return_value = max_min
|
||||
@@ -360,13 +366,15 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
target_temp_high = self.target_temperature_high
|
||||
target_temp_low = self.target_temperature_low
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
|
||||
self.hass, self.target_temperature_high, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
if target_temp_low is not None:
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
data["total_consumption"] = self.wink.total_consumption()
|
||||
data["schedule_enabled"] = self.wink.schedule_enabled()
|
||||
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
||||
self.hass, self.target_temperature_low, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
data[ATTR_TOTAL_CONSUMPTION] = self.wink.total_consumption()
|
||||
data[ATTR_SCHEDULE_ENABLED] = self.wink.schedule_enabled()
|
||||
|
||||
return data
|
||||
|
||||
@@ -377,11 +385,14 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
"""Return current operation ie. auto_eco, cool_only, fan_only."""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
else:
|
||||
current_op = WINK_STATE_TO_HA.get(self.wink.current_hvac_mode())
|
||||
wink_mode = self.wink.current_mode()
|
||||
if wink_mode == "auto_eco":
|
||||
wink_mode = "eco"
|
||||
current_op = WINK_STATE_TO_HA.get(wink_mode)
|
||||
if current_op is None:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
@@ -392,11 +403,13 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
op_list = ['off']
|
||||
modes = self.wink.modes()
|
||||
for mode in modes:
|
||||
if mode == "auto_eco":
|
||||
mode = "eco"
|
||||
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 + \
|
||||
error = "Invalid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
@@ -420,15 +433,19 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the current fan mode."""
|
||||
"""
|
||||
Return the current fan mode.
|
||||
|
||||
The official Wink app only supports 3 modes [low, medium, high]
|
||||
which are equal to [0.33, 0.66, 1.0] respectively.
|
||||
"""
|
||||
speed = self.wink.current_fan_speed()
|
||||
if speed <= 0.4 and speed > 0.3:
|
||||
if speed <= 0.33:
|
||||
return SPEED_LOW
|
||||
elif speed <= 0.8 and speed > 0.5:
|
||||
elif speed <= 0.66:
|
||||
return SPEED_MEDIUM
|
||||
elif speed <= 1.0 and speed > 0.8:
|
||||
else:
|
||||
return SPEED_HIGH
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
@@ -436,11 +453,16 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set fan speed."""
|
||||
"""
|
||||
Set fan speed.
|
||||
|
||||
The official Wink app only supports 3 modes [low, medium, high]
|
||||
which are equal to [0.33, 0.66, 1.0] respectively.
|
||||
"""
|
||||
if fan == SPEED_LOW:
|
||||
speed = 0.4
|
||||
speed = 0.33
|
||||
elif fan == SPEED_MEDIUM:
|
||||
speed = 0.8
|
||||
speed = 0.66
|
||||
elif fan == SPEED_HIGH:
|
||||
speed = 1.0
|
||||
self.wink.set_ac_fan_speed(speed)
|
||||
@@ -459,8 +481,8 @@ class WinkWaterHeater(WinkDevice, ClimateDevice):
|
||||
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()
|
||||
data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled()
|
||||
data[ATTR_RHEEM_TYPE] = self.wink.rheem_type()
|
||||
|
||||
return data
|
||||
|
||||
@@ -492,7 +514,7 @@ class WinkWaterHeater(WinkDevice, ClimateDevice):
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invaid operation mode mapping. " + mode + \
|
||||
error = "Invalid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
|
||||
@@ -1,47 +1,188 @@
|
||||
"""Component to integrate the Home Assistant cloud."""
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
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 homeassistant.helpers import entityfilter
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.alexa import smart_home
|
||||
|
||||
from . import http_api, iot
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
|
||||
REQUIREMENTS = ['warrant==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ALEXA = 'alexa'
|
||||
CONF_ALEXA_FILTER = 'filter'
|
||||
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']
|
||||
|
||||
ALEXA_SCHEMA = vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_ALEXA_FILTER,
|
||||
default=lambda: entityfilter.generate_filter([], [], [], [])
|
||||
): entityfilter.FILTER_SCHEMA,
|
||||
})
|
||||
|
||||
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,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
|
||||
}),
|
||||
}, 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
|
||||
if CONF_ALEXA not in kwargs:
|
||||
kwargs[CONF_ALEXA] = ALEXA_SCHEMA({})
|
||||
|
||||
data = hass.data[DOMAIN] = {
|
||||
'mode': mode
|
||||
}
|
||||
kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA])
|
||||
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
||||
|
||||
data['auth'] = yield from hass.async_add_job(auth_api.load_auth, hass)
|
||||
@asyncio.coroutine
|
||||
def init_cloud(event):
|
||||
"""Initialize connection."""
|
||||
yield from cloud.initialize()
|
||||
|
||||
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, alexa=None):
|
||||
"""Create an instance of Cloud."""
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
self.alexa_config = alexa
|
||||
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.id_token is not None
|
||||
|
||||
@property
|
||||
def subscription_expired(self):
|
||||
"""Return a boolen if the subscription has expired."""
|
||||
# For now, don't enforce subscriptions to exist
|
||||
if 'custom:sub-exp' not in self.claims:
|
||||
return False
|
||||
|
||||
return dt_util.utcnow() > self.expiration_date
|
||||
|
||||
@property
|
||||
def expiration_date(self):
|
||||
"""Return the subscription expiration as a UTC datetime object."""
|
||||
return datetime.combine(
|
||||
dt_util.parse_date(self.claims['custom:sub-exp']),
|
||||
datetime.min.time()).replace(tzinfo=dt_util.UTC)
|
||||
|
||||
@property
|
||||
def claims(self):
|
||||
"""Get the claims from the id token."""
|
||||
from jose import jwt
|
||||
return jwt.get_unverified_claims(self.id_token)
|
||||
|
||||
@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.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.id_token is not None:
|
||||
yield from self.iot.connect()
|
||||
|
||||
def path(self, *parts):
|
||||
"""Get config path inside cloud dir.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
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.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({
|
||||
'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,119 @@ 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.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,19 @@
|
||||
"""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': ''
|
||||
# }
|
||||
}
|
||||
|
||||
MESSAGE_EXPIRATION = """
|
||||
It looks like your Home Assistant Cloud subscription has expired. Please check
|
||||
your [account page](/config/cloud/account) to continue using the service.
|
||||
"""
|
||||
|
||||
@@ -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,16 @@ 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'])
|
||||
|
||||
return self.json(_auth_data(auth))
|
||||
hass.async_add_job(cloud.iot.connect)
|
||||
# Allow cloud to start connecting.
|
||||
yield from asyncio.sleep(0, loop=hass.loop)
|
||||
return self.json(_account_data(cloud))
|
||||
|
||||
|
||||
class CloudLogoutView(HomeAssistantView):
|
||||
@@ -94,10 +97,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 +115,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 +138,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 +162,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 +186,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 +211,23 @@ 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."""
|
||||
claims = cloud.claims
|
||||
|
||||
return {
|
||||
'email': auth.account.email
|
||||
'email': claims['email'],
|
||||
'sub_exp': claims.get('custom:sub-exp'),
|
||||
'cloud': cloud.iot.state,
|
||||
}
|
||||
|
||||
227
homeassistant/components/cloud/iot.py
Normal file
227
homeassistant/components/cloud/iot.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""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
|
||||
from .const import MESSAGE_EXPIRATION
|
||||
|
||||
|
||||
HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_CONNECTING = 'connecting'
|
||||
STATE_CONNECTED = 'connected'
|
||||
STATE_DISCONNECTED = 'disconnected'
|
||||
|
||||
|
||||
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
|
||||
# The WebSocket client
|
||||
self.client = None
|
||||
# Scheduled sleep task till next connection retry
|
||||
self.retry_task = None
|
||||
# Boolean to indicate if we wanted the connection to close
|
||||
self.close_requested = False
|
||||
# The current number of attempts to connect, impacts wait time
|
||||
self.tries = 0
|
||||
# Current state of the connection
|
||||
self.state = STATE_DISCONNECTED
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
"""Connect to the IoT broker."""
|
||||
hass = self.cloud.hass
|
||||
if self.cloud.subscription_expired:
|
||||
# Try refreshing the token to see if it is still expired.
|
||||
yield from hass.async_add_job(auth_api.check_token, self.cloud)
|
||||
|
||||
if self.cloud.subscription_expired:
|
||||
hass.components.persistent_notification.async_create(
|
||||
MESSAGE_EXPIRATION, 'Subscription expired',
|
||||
'cloud_subscription_expired')
|
||||
self.state = STATE_DISCONNECTED
|
||||
return
|
||||
|
||||
if self.state == STATE_CONNECTED:
|
||||
raise RuntimeError('Already connected')
|
||||
|
||||
self.state = STATE_CONNECTING
|
||||
self.close_requested = False
|
||||
remove_hass_stop_listener = None
|
||||
session = async_get_clientsession(self.cloud.hass)
|
||||
client = None
|
||||
disconnect_warn = None
|
||||
|
||||
@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()
|
||||
|
||||
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')
|
||||
self.state = STATE_CONNECTED
|
||||
|
||||
while not client.closed:
|
||||
msg = yield from client.receive()
|
||||
|
||||
if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED,
|
||||
WSMsgType.CLOSING):
|
||||
disconnect_warn = 'Connection cancelled.'
|
||||
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 self.close_requested:
|
||||
self.state = STATE_DISCONNECTED
|
||||
|
||||
else:
|
||||
self.state = STATE_CONNECTING
|
||||
self.tries += 1
|
||||
|
||||
try:
|
||||
# Sleep 0, 5, 10, 15 … up to 30 seconds between retries
|
||||
self.retry_task = hass.async_add_job(asyncio.sleep(
|
||||
min(30, (self.tries - 1) * 5), loop=hass.loop))
|
||||
yield from self.retry_task
|
||||
self.retry_task = None
|
||||
hass.async_add_job(self.connect())
|
||||
except asyncio.CancelledError:
|
||||
# Happens if disconnect called
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
def disconnect(self):
|
||||
"""Disconnect the client."""
|
||||
self.close_requested = True
|
||||
|
||||
if self.client is not None:
|
||||
yield from self.client.close()
|
||||
elif self.retry_task is not None:
|
||||
self.retry_task.cancel()
|
||||
|
||||
|
||||
@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,
|
||||
cloud.alexa_config,
|
||||
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):
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
"""Provide configuration end points for Z-Wave."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from collections import deque
|
||||
from aiohttp.web import Response
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import HTTP_NOT_FOUND
|
||||
from homeassistant.const import HTTP_NOT_FOUND, HTTP_OK
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.config import EditKeyBasedConfigView
|
||||
from homeassistant.components.zwave import const, DEVICE_CONFIG_SCHEMA_ENTRY
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONFIG_PATH = 'zwave_device_config.yaml'
|
||||
OZW_LOG_FILENAME = 'OZW_Log.txt'
|
||||
URL_API_OZW_LOG = '/api/zwave/ozwlog'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -25,12 +27,64 @@ def async_setup(hass):
|
||||
hass.http.register_view(ZWaveNodeGroupView)
|
||||
hass.http.register_view(ZWaveNodeConfigView)
|
||||
hass.http.register_view(ZWaveUserCodeView)
|
||||
hass.http.register_static_path(
|
||||
URL_API_OZW_LOG, hass.config.path(OZW_LOG_FILENAME), False)
|
||||
hass.http.register_view(ZWaveLogView)
|
||||
hass.http.register_view(ZWaveConfigWriteView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ZWaveLogView(HomeAssistantView):
|
||||
"""View to read the ZWave log file."""
|
||||
|
||||
url = "/api/zwave/ozwlog"
|
||||
name = "api:zwave:ozwlog"
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Retrieve the lines from ZWave log."""
|
||||
try:
|
||||
lines = int(request.query.get('lines', 0))
|
||||
except ValueError:
|
||||
return Response(text='Invalid datetime', status=400)
|
||||
|
||||
hass = request.app['hass']
|
||||
response = yield from hass.async_add_job(self._get_log, hass, lines)
|
||||
|
||||
return Response(text='\n'.join(response))
|
||||
|
||||
def _get_log(self, hass, lines):
|
||||
"""Retrieve the logfile content."""
|
||||
logfilepath = hass.config.path(OZW_LOG_FILENAME)
|
||||
with open(logfilepath, 'r') as logfile:
|
||||
data = (line.rstrip() for line in logfile)
|
||||
if lines == 0:
|
||||
loglines = list(data)
|
||||
else:
|
||||
loglines = deque(data, lines)
|
||||
return loglines
|
||||
|
||||
|
||||
class ZWaveConfigWriteView(HomeAssistantView):
|
||||
"""View to save the ZWave configuration to zwcfg_xxxxx.xml."""
|
||||
|
||||
url = "/api/zwave/saveconfig"
|
||||
name = "api:zwave:saveconfig"
|
||||
|
||||
@ha.callback
|
||||
def post(self, request):
|
||||
"""Save cache configuration to zwcfg_xxxxx.xml."""
|
||||
hass = request.app['hass']
|
||||
network = hass.data.get(const.DATA_NETWORK)
|
||||
if network is None:
|
||||
return self.json_message('No Z-Wave network data found',
|
||||
HTTP_NOT_FOUND)
|
||||
_LOGGER.info("Z-Wave configuration written to file.")
|
||||
network.write_config()
|
||||
return self.json_message('Z-Wave configuration saved to file.',
|
||||
HTTP_OK)
|
||||
|
||||
|
||||
class ZWaveNodeValueView(HomeAssistantView):
|
||||
"""View to return the node values."""
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ class Configurator(object):
|
||||
|
||||
self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove)
|
||||
|
||||
@async_callback
|
||||
@asyncio.coroutine
|
||||
def async_handle_service_call(self, call):
|
||||
"""Handle a configure service call."""
|
||||
request_id = call.data.get(ATTR_CONFIGURE_ID)
|
||||
@@ -220,7 +220,8 @@ class Configurator(object):
|
||||
|
||||
# field validation goes here?
|
||||
if callback:
|
||||
self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
|
||||
yield from self.hass.async_add_job(callback,
|
||||
call.data.get(ATTR_FIELDS, {}))
|
||||
|
||||
def _generate_unique_id(self):
|
||||
"""Generate a unique configurator ID."""
|
||||
|
||||
@@ -140,13 +140,13 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_INCREMENT, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_INCREMENT], SERVICE_SCHEMA)
|
||||
descriptions[SERVICE_INCREMENT], SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DECREMENT, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_DECREMENT], SERVICE_SCHEMA)
|
||||
descriptions[SERVICE_DECREMENT], SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESET, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_RESET], SERVICE_SCHEMA)
|
||||
descriptions[SERVICE_RESET], SERVICE_SCHEMA)
|
||||
|
||||
yield from component.async_add_entities(entities)
|
||||
return True
|
||||
20
homeassistant/components/counter/services.yaml
Normal file
20
homeassistant/components/counter/services.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# Describes the format for available counter services
|
||||
|
||||
decrement:
|
||||
description: Decrement a counter.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id of the counter to decrement.
|
||||
example: 'counter.count0'
|
||||
increment:
|
||||
description: Increment a counter.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id of the counter to increment.
|
||||
example: 'counter.count0'
|
||||
reset:
|
||||
description: Reset a counter.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id of the counter to reset.
|
||||
example: 'counter.count0'
|
||||
@@ -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)
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for Lutron Caseta shades.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.lutron_caseta/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
@@ -18,7 +19,8 @@ DEPENDENCIES = ['lutron_caseta']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Lutron Caseta shades as a cover device."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
@@ -27,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
dev = LutronCasetaCover(cover_device, bridge)
|
||||
devs.append(dev)
|
||||
|
||||
add_devices(devs, True)
|
||||
async_add_devices(devs, True)
|
||||
|
||||
|
||||
class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
||||
@@ -48,21 +50,25 @@ class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
||||
"""Return the current position of cover."""
|
||||
return self._state['current_state']
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 0)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 100)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Move the shade to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
self._smartbridge.set_value(self._device_id, position)
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Call when forcing a refresh of the device."""
|
||||
self._state = self._smartbridge.get_device_by_id(self._device_id)
|
||||
_LOGGER.debug(self._state)
|
||||
|
||||
@@ -104,6 +104,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT Cover."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
@@ -103,6 +103,11 @@ class RflinkCover(RflinkCommand, CoverDevice):
|
||||
"""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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -77,6 +76,7 @@ ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_SOURCE_TYPE = 'source_type'
|
||||
ATTR_VENDOR = 'vendor'
|
||||
|
||||
SOURCE_TYPE_GPS = 'gps'
|
||||
SOURCE_TYPE_ROUTER = 'router'
|
||||
@@ -89,10 +89,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 +176,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."""
|
||||
@@ -299,11 +286,6 @@ class DeviceTracker(object):
|
||||
if device.track:
|
||||
yield from device.async_update_ha_state()
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
})
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group and self.track_new:
|
||||
self.group.async_set_group(
|
||||
@@ -313,6 +295,13 @@ class DeviceTracker(object):
|
||||
# lookup mac vendor string to be stored in config
|
||||
yield from device.set_vendor_for_mac()
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
ATTR_MAC: device.mac,
|
||||
ATTR_VENDOR: device.vendor,
|
||||
})
|
||||
|
||||
# update known_devices.yaml
|
||||
self.hass.async_add_job(
|
||||
self.async_update_config(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
138
homeassistant/components/device_tracker/hitron_coda.py
Normal file
138
homeassistant/components/device_tracker/hitron_coda.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Support for the Hitron CODA-4582U, provided by Rogers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.hitron_coda/
|
||||
"""
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
import requests
|
||||
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
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(_hass, config):
|
||||
"""Validate the configuration and return a Nmap scanner."""
|
||||
scanner = HitronCODADeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['mac', 'name'])
|
||||
|
||||
|
||||
class HitronCODADeviceScanner(DeviceScanner):
|
||||
"""This class scans for devices using the CODA's web interface."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.last_results = []
|
||||
host = config[CONF_HOST]
|
||||
self._url = 'http://{}/data/getConnectInfo.asp'.format(host)
|
||||
self._loginurl = 'http://{}/goform/login'.format(host)
|
||||
|
||||
self._username = config.get(CONF_USERNAME)
|
||||
self._password = config.get(CONF_PASSWORD)
|
||||
|
||||
self._userid = None
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info("Scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name of the device with the given MAC address."""
|
||||
name = next((
|
||||
device.name for device in self.last_results
|
||||
if device.mac == mac), None)
|
||||
return name
|
||||
|
||||
def _login(self):
|
||||
"""Log in to the router. This is required for subsequent api calls."""
|
||||
_LOGGER.info("Logging in to CODA...")
|
||||
|
||||
try:
|
||||
data = [
|
||||
('user', self._username),
|
||||
('pws', self._password),
|
||||
]
|
||||
res = requests.post(self._loginurl, data=data, timeout=10)
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.error(
|
||||
"Connection to the router timed out at URL %s", self._url)
|
||||
return False
|
||||
if res.status_code != 200:
|
||||
_LOGGER.error(
|
||||
"Connection failed with http code %s", res.status_code)
|
||||
return False
|
||||
try:
|
||||
self._userid = res.cookies['userid']
|
||||
return True
|
||||
except KeyError:
|
||||
_LOGGER.error("Failed to log in to router")
|
||||
return False
|
||||
|
||||
def _update_info(self):
|
||||
"""Get ARP from router."""
|
||||
_LOGGER.info("Fetching...")
|
||||
|
||||
if self._userid is None:
|
||||
if not self._login():
|
||||
_LOGGER.error("Could not obtain a user ID from the router")
|
||||
return False
|
||||
last_results = []
|
||||
|
||||
# doing a request
|
||||
try:
|
||||
res = requests.get(self._url, timeout=10, cookies={
|
||||
'userid': self._userid
|
||||
})
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.error(
|
||||
"Connection to the router timed out at URL %s", self._url)
|
||||
return False
|
||||
if res.status_code != 200:
|
||||
_LOGGER.error(
|
||||
"Connection failed with http code %s", res.status_code)
|
||||
return False
|
||||
try:
|
||||
result = res.json()
|
||||
except ValueError:
|
||||
# If json decoder could not parse the response
|
||||
_LOGGER.error("Failed to parse response from router")
|
||||
return False
|
||||
|
||||
# parsing response
|
||||
for info in result:
|
||||
mac = info['macAddr']
|
||||
name = info['hostName']
|
||||
# No address = no item :)
|
||||
if mac is None:
|
||||
continue
|
||||
|
||||
last_results.append(Device(mac.upper(), name))
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info("Request successful")
|
||||
return True
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -5,23 +5,22 @@ 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 slugify, decorator
|
||||
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__)
|
||||
|
||||
@@ -34,6 +33,8 @@ CONF_SECRET = 'secret'
|
||||
CONF_WAYPOINT_IMPORT = 'waypoints'
|
||||
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
OWNTRACKS_TOPIC = 'owntracks/#'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -74,6 +75,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
_LOGGER.error("Unable to parse payload as JSON: %s", payload)
|
||||
return
|
||||
|
||||
message['topic'] = topic
|
||||
|
||||
@@ -90,7 +92,11 @@ def _parse_topic(topic):
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
_, user, device, *_ = topic.split('/', 3)
|
||||
try:
|
||||
_, user, device, *_ = topic.split('/', 3)
|
||||
except ValueError:
|
||||
_LOGGER.error("Can't parse topic: '%s'", topic)
|
||||
raise
|
||||
|
||||
return user, device
|
||||
|
||||
@@ -193,7 +199,7 @@ class OwnTracksContext:
|
||||
self.async_see = async_see
|
||||
self.secret = secret
|
||||
self.max_gps_accuracy = max_gps_accuracy
|
||||
self.mobile_beacons_active = defaultdict(list)
|
||||
self.mobile_beacons_active = defaultdict(set)
|
||||
self.regions_entered = defaultdict(list)
|
||||
self.import_waypoints = import_waypoints
|
||||
self.waypoint_whitelist = waypoint_whitelist
|
||||
@@ -228,10 +234,25 @@ class OwnTracksContext:
|
||||
return True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_see_beacons(self, dev_id, kwargs_param):
|
||||
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)
|
||||
@@ -255,7 +276,7 @@ def async_handle_location_message(hass, context, message):
|
||||
return
|
||||
|
||||
yield from context.async_see(**kwargs)
|
||||
yield from context.async_see_beacons(dev_id, kwargs)
|
||||
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -265,11 +286,15 @@ def _async_transition_message_enter(hass, context, message, 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 assume mobile
|
||||
# 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.append(location)
|
||||
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]
|
||||
@@ -277,9 +302,8 @@ def _async_transition_message_enter(hass, context, message, location):
|
||||
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(dev_id, kwargs)
|
||||
yield from context.async_see(**kwargs)
|
||||
yield from context.async_see_beacons(hass, dev_id, kwargs)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -291,30 +315,29 @@ def _async_transition_message_leave(hass, context, message, location):
|
||||
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)
|
||||
yield from context.async_see(**kwargs)
|
||||
yield from context.async_see_beacons(dev_id, kwargs)
|
||||
return
|
||||
|
||||
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(dev_id, kwargs)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@HANDLERS.register('transition')
|
||||
@@ -344,6 +367,29 @@ def async_handle_transition_message(hass, context, message):
|
||||
message['event'])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_waypoint(hass, name_base, waypoint):
|
||||
"""Handle a waypoint."""
|
||||
name = waypoint['desc']
|
||||
pretty_name = '{} - {}'.format(name_base, name)
|
||||
lat = waypoint['lat']
|
||||
lon = waypoint['lon']
|
||||
rad = waypoint['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:
|
||||
return
|
||||
|
||||
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('waypoint')
|
||||
@HANDLERS.register('waypoints')
|
||||
@asyncio.coroutine
|
||||
def async_handle_waypoints_message(hass, context, message):
|
||||
@@ -357,30 +403,17 @@ def async_handle_waypoints_message(hass, context, message):
|
||||
if user not in context.waypoint_whitelist:
|
||||
return
|
||||
|
||||
wayps = message['waypoints']
|
||||
if 'waypoints' in message:
|
||||
wayps = message['waypoints']
|
||||
else:
|
||||
wayps = [message]
|
||||
|
||||
_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()
|
||||
yield from async_handle_waypoint(hass, name_base, wayp)
|
||||
|
||||
|
||||
@HANDLERS.register('encrypted')
|
||||
@@ -399,15 +432,30 @@ def async_handle_encrypted_message(hass, context, message):
|
||||
yield from async_handle_message(hass, context, decrypted)
|
||||
|
||||
|
||||
@HANDLERS.register('lwt')
|
||||
@HANDLERS.register('configuration')
|
||||
@HANDLERS.register('beacon')
|
||||
@HANDLERS.register('cmd')
|
||||
@HANDLERS.register('steps')
|
||||
@HANDLERS.register('card')
|
||||
@asyncio.coroutine
|
||||
def async_handle_not_impl_msg(hass, context, message):
|
||||
"""Handle valid but not implemented message types."""
|
||||
_LOGGER.debug('Not handling %s message: %s', message.get("_type"), message)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_unsupported_msg(hass, context, message):
|
||||
"""Handle an unsupported or invalid message type."""
|
||||
_LOGGER.warning('Received unsupported message type: %s.',
|
||||
message.get('_type'))
|
||||
|
||||
|
||||
@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:
|
||||
error = 'Received unsupported message type: {}.'.format(msgtype)
|
||||
_LOGGER.warning(error)
|
||||
handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
|
||||
|
||||
yield from handler(hass, context, message)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -14,29 +14,29 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.4.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.3.9']
|
||||
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
CONF_PRIVKEY = 'privkey'
|
||||
CONF_BASEOID = 'baseoid'
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_PRIVKEY = 'privkey'
|
||||
|
||||
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
|
||||
|
||||
@@ -6,13 +6,14 @@ https://home-assistant.io/components/device_tracker.swisscom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import requests
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,7 +78,7 @@ class SwisscomDeviceScanner(DeviceScanner):
|
||||
def get_swisscom_data(self):
|
||||
"""Retrieve data from Swisscom and return parsed result."""
|
||||
url = 'http://{}/ws'.format(self.host)
|
||||
headers = {'Content-Type': 'application/x-sah-ws-4-call+json'}
|
||||
headers = {CONTENT_TYPE: 'application/x-sah-ws-4-call+json'}
|
||||
data = """
|
||||
{"service":"Devices", "method":"get",
|
||||
"parameters":{"expression":"lan and not self"}}"""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user