mirror of
https://github.com/home-assistant/core.git
synced 2026-01-19 22:16:59 +01:00
Compare commits
357 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6745e83a6c | ||
|
|
44bc057fdb | ||
|
|
96b8d8fcfa | ||
|
|
fc2df34206 | ||
|
|
09c29737de | ||
|
|
4c01b47945 | ||
|
|
080f56e0f5 | ||
|
|
173e15e733 | ||
|
|
72407c2f95 | ||
|
|
1b79722b69 | ||
|
|
cc5233103c | ||
|
|
2feea1d1eb | ||
|
|
2c39c39d52 | ||
|
|
6e6b1ef7ab | ||
|
|
55ddaf1ee7 | ||
|
|
6860d9b096 | ||
|
|
3e1cc4282e | ||
|
|
200bdb30ff | ||
|
|
eb17ba970c | ||
|
|
ffe4c425af | ||
|
|
a18fdbfbb8 | ||
|
|
58600f25b3 | ||
|
|
fc3235fb6d | ||
|
|
d129df93dd | ||
|
|
0af1a96f14 | ||
|
|
272899ec96 | ||
|
|
6a92e27e2f | ||
|
|
faceb4c1dc | ||
|
|
6d5f00098a | ||
|
|
08f75f7935 | ||
|
|
af297aa0dc | ||
|
|
20e1b3eae0 | ||
|
|
28861221ae | ||
|
|
f367c49fb9 | ||
|
|
4770888d22 | ||
|
|
1d0f3b930f | ||
|
|
22e2262f8e | ||
|
|
53d1a040d4 | ||
|
|
d7d71c97e2 | ||
|
|
c15fd4323e | ||
|
|
91227d9a2e | ||
|
|
a3db0ec231 | ||
|
|
18e965c3cd | ||
|
|
4cc417677e | ||
|
|
525d735f21 | ||
|
|
e88b98f5fa | ||
|
|
6f68752d1e | ||
|
|
61a0976752 | ||
|
|
d7b3c9c38e | ||
|
|
a01939c6e9 | ||
|
|
c128919b5f | ||
|
|
e5d69feb93 | ||
|
|
ee5f228309 | ||
|
|
15dde7925a | ||
|
|
fcf318cf53 | ||
|
|
c2a5f63b1f | ||
|
|
79fa2d4175 | ||
|
|
214a18f08c | ||
|
|
ded2ea8b19 | ||
|
|
1d100dcac9 | ||
|
|
a3ae96440b | ||
|
|
0235626f40 | ||
|
|
d7dd7df5e7 | ||
|
|
1e28851280 | ||
|
|
df68de8032 | ||
|
|
f3595f790a | ||
|
|
0d14920758 | ||
|
|
2940fb72fb | ||
|
|
8e0838adeb | ||
|
|
4e820ea30a | ||
|
|
26490109ac | ||
|
|
e4a713207d | ||
|
|
cc0d0a38d7 | ||
|
|
afde5a6b26 | ||
|
|
a5fb284717 | ||
|
|
e487a09190 | ||
|
|
52eb816c62 | ||
|
|
90d894a499 | ||
|
|
ba13951fff | ||
|
|
2a7b7ebd6a | ||
|
|
df7d9c3bb2 | ||
|
|
c549ea115d | ||
|
|
dad54bb993 | ||
|
|
0211cf29eb | ||
|
|
1d9ac5f8b3 | ||
|
|
7f699b4261 | ||
|
|
a1e910f1cf | ||
|
|
b4899ec469 | ||
|
|
06de7053ce | ||
|
|
4484a7a94b | ||
|
|
a89e635bf3 | ||
|
|
3ab056ba69 | ||
|
|
5ba815ab21 | ||
|
|
274e9799b3 | ||
|
|
5ce9aea65d | ||
|
|
705814cb08 | ||
|
|
8e695d1eb0 | ||
|
|
be272ac64a | ||
|
|
b910a9917d | ||
|
|
9649097b32 | ||
|
|
5e76a51db4 | ||
|
|
27abac85b6 | ||
|
|
9f2aae1357 | ||
|
|
e6ece4bf6d | ||
|
|
aea2d1b317 | ||
|
|
33e46b484f | ||
|
|
3f6a5564ad | ||
|
|
3317b4916b | ||
|
|
9c0455e3dc | ||
|
|
5d43d3eb1c | ||
|
|
4163e55dbd | ||
|
|
3cc4fdaa34 | ||
|
|
edeb31d74e | ||
|
|
54d19e3c53 | ||
|
|
892f455aee | ||
|
|
942d630762 | ||
|
|
9ea1101aba | ||
|
|
08a65a3b31 | ||
|
|
d4b3f56d53 | ||
|
|
5a2b4a5376 | ||
|
|
9d836a115a | ||
|
|
bf92aedd38 | ||
|
|
230c3815f2 | ||
|
|
9afe066ec8 | ||
|
|
66541a6a19 | ||
|
|
825ee3612d | ||
|
|
65bd7d2326 | ||
|
|
d8c1013b09 | ||
|
|
02d1dc6247 | ||
|
|
726d950522 | ||
|
|
3324995e70 | ||
|
|
85747fe2ef | ||
|
|
09db875ace | ||
|
|
7d407756c3 | ||
|
|
91d682d02c | ||
|
|
b75c103db4 | ||
|
|
bba323d226 | ||
|
|
7564d539c1 | ||
|
|
33439aaa22 | ||
|
|
d5368f6f78 | ||
|
|
d9999f36e8 | ||
|
|
235e1a0885 | ||
|
|
541fec0534 | ||
|
|
b3ad7989ae | ||
|
|
3d897e0e52 | ||
|
|
c6d5987109 | ||
|
|
5d3956ea98 | ||
|
|
4fb0b27310 | ||
|
|
4833e992fb | ||
|
|
fe3aed0f0c | ||
|
|
57402bcb43 | ||
|
|
7f48c00793 | ||
|
|
f58647849a | ||
|
|
1f468fc94d | ||
|
|
961c02f72a | ||
|
|
79da1ec0d9 | ||
|
|
fe174402d2 | ||
|
|
1b2dfb8ed1 | ||
|
|
297a6f6f03 | ||
|
|
0dfcf40d37 | ||
|
|
d308ea69ce | ||
|
|
a8c5c995a0 | ||
|
|
86b318e992 | ||
|
|
53ea926292 | ||
|
|
2b1f4123db | ||
|
|
b36e346ccb | ||
|
|
89e8fb4066 | ||
|
|
e2d23d902a | ||
|
|
2604dd89a6 | ||
|
|
044b9caa76 | ||
|
|
1707cdf9f3 | ||
|
|
4c86721e70 | ||
|
|
0ff500ca25 | ||
|
|
23f54b07c7 | ||
|
|
f25ddef4d7 | ||
|
|
7158919346 | ||
|
|
627517cbbc | ||
|
|
4ecfc7d066 | ||
|
|
72751b95b5 | ||
|
|
fc3b7907ed | ||
|
|
f26a7fc6bb | ||
|
|
0c563f7b14 | ||
|
|
519d9f2fd0 | ||
|
|
3701ac292c | ||
|
|
1db18478d2 | ||
|
|
3230869f74 | ||
|
|
9aa88819a5 | ||
|
|
3e92318cb2 | ||
|
|
f0a38dded6 | ||
|
|
c32f47aea6 | ||
|
|
626763a7c3 | ||
|
|
5df8477536 | ||
|
|
1f89e6ddba | ||
|
|
13ab2be5f6 | ||
|
|
2bc84af87e | ||
|
|
ef2ed7bfc9 | ||
|
|
6040a40af2 | ||
|
|
fb352c20d9 | ||
|
|
678f30def1 | ||
|
|
0fce5ccc7f | ||
|
|
02afc98668 | ||
|
|
57777ef79a | ||
|
|
ca6fa1313e | ||
|
|
ea91d24eb2 | ||
|
|
6e5a3c0a94 | ||
|
|
754d536974 | ||
|
|
4f6ed09a99 | ||
|
|
8d375e2d47 | ||
|
|
1d2d338cd0 | ||
|
|
cb47507002 | ||
|
|
fae620f3b3 | ||
|
|
b821a82417 | ||
|
|
da7837af73 | ||
|
|
6e903fd429 | ||
|
|
9f7e167669 | ||
|
|
54a64fb8d9 | ||
|
|
2d89c3ecf4 | ||
|
|
9f6d1c4e7b | ||
|
|
62b8e54235 | ||
|
|
1ceac8407d | ||
|
|
0c0b02eb3d | ||
|
|
c70722dbae | ||
|
|
a05fb4cef8 | ||
|
|
fee01fcccc | ||
|
|
2b37b4251b | ||
|
|
3aa1b6a3f8 | ||
|
|
c32afcd961 | ||
|
|
d60c2d604f | ||
|
|
081e61528d | ||
|
|
5799d1aec9 | ||
|
|
7d32e5eeeb | ||
|
|
57f32fa629 | ||
|
|
7da47852d4 | ||
|
|
f1b658ea5d | ||
|
|
754e93ff6a | ||
|
|
947c1efca2 | ||
|
|
a1239077d9 | ||
|
|
53b5dc8e84 | ||
|
|
09bcd7321a | ||
|
|
14ef0ca786 | ||
|
|
272539105f | ||
|
|
7d67017de7 | ||
|
|
d921073e77 | ||
|
|
9cf2acb495 | ||
|
|
3b424b034a | ||
|
|
76598bc4d2 | ||
|
|
c54476b62f | ||
|
|
ae8a8e22ad | ||
|
|
4c8d1d9d2f | ||
|
|
daea93d9f9 | ||
|
|
1540bb1279 | ||
|
|
555e533f67 | ||
|
|
118f2f0bad | ||
|
|
c8add59ea5 | ||
|
|
18f5258aaf | ||
|
|
207c9e8575 | ||
|
|
4891ca1610 | ||
|
|
10c9132046 | ||
|
|
71ee847aee | ||
|
|
d9ae7ceb0c | ||
|
|
2a972b2334 | ||
|
|
7484152be1 | ||
|
|
6581dc2381 | ||
|
|
31ec0ac6a7 | ||
|
|
8b2edc1514 | ||
|
|
2612c6d6b8 | ||
|
|
0b8b9ecb94 | ||
|
|
a0fdb2778d | ||
|
|
5ef8ca9b03 | ||
|
|
a10fa90357 | ||
|
|
9743e17d62 | ||
|
|
6fcb1b548e | ||
|
|
1bf5554017 | ||
|
|
49b1643ff0 | ||
|
|
ce19e6367f | ||
|
|
180e146e14 | ||
|
|
bead274b20 | ||
|
|
1cbf8c8049 | ||
|
|
ad259ead50 | ||
|
|
bb457f47cc | ||
|
|
df3e904fe7 | ||
|
|
7697cdef0a | ||
|
|
6951b6f60b | ||
|
|
6ca0d4cd14 | ||
|
|
a5b756e1e5 | ||
|
|
7848791a04 | ||
|
|
f916fc04f9 | ||
|
|
8f4608c654 | ||
|
|
399a0b470a | ||
|
|
4d716cec2b | ||
|
|
1373db8b60 | ||
|
|
7771cc2ccf | ||
|
|
c5ad7996fb | ||
|
|
3d94f77998 | ||
|
|
6330d9cde6 | ||
|
|
9a0bb62654 | ||
|
|
d873a7baf0 | ||
|
|
c663d85129 | ||
|
|
8c13d3ed4c | ||
|
|
cb322f72db | ||
|
|
39a446c43c | ||
|
|
aa8622f8e8 | ||
|
|
e031b8078f | ||
|
|
e1647fb6ac | ||
|
|
10feac11d9 | ||
|
|
d4e2332ce0 | ||
|
|
1d7169403b | ||
|
|
e4685de459 | ||
|
|
d83de36c32 | ||
|
|
73547c8c4b | ||
|
|
40094cecae | ||
|
|
0b327cd4d9 | ||
|
|
d302dbec2d | ||
|
|
e135691bd6 | ||
|
|
d4dc2707a1 | ||
|
|
a8cdf36d5c | ||
|
|
a99f36f519 | ||
|
|
0568ef025b | ||
|
|
8ded8f572a | ||
|
|
7cf2c48175 | ||
|
|
7cf9ff83bc | ||
|
|
dfe9af7110 | ||
|
|
016e8f833d | ||
|
|
574df0f420 | ||
|
|
f18f181962 | ||
|
|
4754455295 | ||
|
|
8c1317f278 | ||
|
|
7c2cb6cffd | ||
|
|
8c9d1d9af1 | ||
|
|
941fccd3fc | ||
|
|
711526e574 | ||
|
|
a2503e4d13 | ||
|
|
656ee52435 | ||
|
|
002660fd9e | ||
|
|
63580f9e03 | ||
|
|
87d9cdd78f | ||
|
|
c8ca66b671 | ||
|
|
9b98d470c2 | ||
|
|
b19ec21e88 | ||
|
|
3b331eac56 | ||
|
|
552265bc31 | ||
|
|
df58f718ab | ||
|
|
76a1a54369 | ||
|
|
b6e008be71 | ||
|
|
9d5c20b629 | ||
|
|
63d9ea6643 | ||
|
|
1d0df63615 | ||
|
|
154eacef6c | ||
|
|
dc95b28487 | ||
|
|
4d9bac6f9c | ||
|
|
6419d273ea | ||
|
|
7882b19dc5 | ||
|
|
8f8bba4ad7 | ||
|
|
7b40a641ec | ||
|
|
1b26b5ad14 | ||
|
|
bbbb4441ea | ||
|
|
1e2e877302 |
54
.coveragerc
54
.coveragerc
@@ -31,9 +31,15 @@ omit =
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
homeassistant/components/*/ios.py
|
||||
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
homeassistant/components/litejet.py
|
||||
homeassistant/components/*/litejet.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
@@ -95,31 +101,38 @@ omit =
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/ffmpeg.py
|
||||
homeassistant/components/*/ffmpeg.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/mochad.py
|
||||
homeassistant/components/*/mochad.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
@@ -127,10 +140,10 @@ omit =
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bbox.py
|
||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
@@ -144,11 +157,10 @@ omit =
|
||||
homeassistant/components/device_tracker/volvooncall.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/garage_door/rpi_gpio.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/joaoapps_join.py
|
||||
@@ -162,12 +174,14 @@ omit =
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/directv.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
@@ -178,6 +192,7 @@ omit =
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pandora.py
|
||||
homeassistant/components/media_player/philips_js.py
|
||||
homeassistant/components/media_player/pioneer.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/roku.py
|
||||
@@ -197,7 +212,9 @@ omit =
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
homeassistant/components/notify/pushetta.py
|
||||
@@ -209,6 +226,7 @@ omit =
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/telstra.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
@@ -216,13 +234,18 @@ omit =
|
||||
homeassistant/components/openalpr.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
@@ -235,9 +258,12 @@ omit =
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
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/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
@@ -252,8 +278,8 @@ omit =
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
@@ -261,6 +287,7 @@ omit =
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
@@ -270,9 +297,7 @@ omit =
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yahoo_finance.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
@@ -281,20 +306,19 @@ omit =
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/neato.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pilight.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/thermostat/eq3btsmart.py
|
||||
homeassistant/components/thermostat/heatmiser.py
|
||||
homeassistant/components/thermostat/homematic.py
|
||||
homeassistant/components/thermostat/proliphix.py
|
||||
homeassistant/components/thermostat/radiotherm.py
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/zeroconf.py
|
||||
|
||||
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,9 +1,9 @@
|
||||
**Description:**
|
||||
|
||||
|
||||
**Related issue (if applicable):** fixes #
|
||||
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
|
||||
|
||||
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
|
||||
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
|
||||
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
```yaml
|
||||
@@ -13,7 +13,7 @@
|
||||
**Checklist:**
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
|
||||
If the code communicates with devices, web services, or third-party tools:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
|
||||
@@ -2,11 +2,11 @@ sudo: false
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.4"
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=py34
|
||||
- python: "3.4"
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
- python: "3.5"
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=lint
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
|
||||
@@ -19,8 +19,7 @@ RUN script/build_python_openzwave && \
|
||||
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
|
||||
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
# certifi breaks Debian based installs
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install mysqlclient psycopg2 uvloop
|
||||
|
||||
# Copy source
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
homeassistant:
|
||||
# Omitted values in this section will be auto detected using freegeoip.io
|
||||
|
||||
# Location required to calculate the time the sun rises and sets.
|
||||
# Location required to calculate the time the sun rises and sets.
|
||||
# Coordinates are also used for location for weather related components.
|
||||
# Google Maps can be used to determine more precise GPS coordinates.
|
||||
latitude: 32.87336
|
||||
@@ -43,12 +43,10 @@ device_tracker:
|
||||
username: admin
|
||||
password: PASSWORD
|
||||
|
||||
chromecast:
|
||||
|
||||
switch:
|
||||
platform: wemo
|
||||
|
||||
thermostat:
|
||||
climate:
|
||||
platform: nest
|
||||
# Required: username and password that are used to login to the Nest thermostat.
|
||||
username: myemail@mydomain.com
|
||||
@@ -79,7 +77,6 @@ group:
|
||||
entities:
|
||||
- group.awesome_people
|
||||
- group.climate
|
||||
|
||||
kitchen:
|
||||
name: Kitchen
|
||||
entities:
|
||||
@@ -92,52 +89,23 @@ group:
|
||||
- input_boolean.notify_home
|
||||
- camera.demo_camera
|
||||
|
||||
example:
|
||||
|
||||
simple_alarm:
|
||||
# Which light/light group has to flash when a known device comes home
|
||||
known_light: light.Bowl
|
||||
# Which light/light group has to flash red when light turns on while no one home
|
||||
unknown_light: group.living_room
|
||||
|
||||
browser:
|
||||
|
||||
keyboard:
|
||||
|
||||
# https://home-assistant.io/getting-started/automation/
|
||||
automation:
|
||||
- alias: 'Rule 1 Light on in the evening'
|
||||
trigger:
|
||||
- platform: sun
|
||||
- alias: Turn on light when sun sets
|
||||
trigger:
|
||||
platform: sun
|
||||
event: sunset
|
||||
offset: "-01:00:00"
|
||||
- platform: state
|
||||
condition:
|
||||
condition: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
condition:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
- platform: time
|
||||
after: "16:00:00"
|
||||
before: "23:00:00"
|
||||
action:
|
||||
service: homeassistant.turn_on
|
||||
entity_id: group.living_room
|
||||
state: 'home'
|
||||
action:
|
||||
service: light.turn_on
|
||||
|
||||
- alias: 'Rule 2 - Away Mode'
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: 'not_home'
|
||||
|
||||
condition: use_trigger_values
|
||||
action:
|
||||
service: light.turn_off
|
||||
entity_id: group.all_lights
|
||||
|
||||
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
|
||||
# Each sensor label should be unique or your sensors might not load correctly.
|
||||
# Another way to do is to collect all entries under one "sensor:"
|
||||
# sensor:
|
||||
# - platform: mqtt
|
||||
@@ -154,34 +122,30 @@ sensor:
|
||||
arg: '/'
|
||||
- type: 'disk_use_percent'
|
||||
arg: '/home'
|
||||
- type: 'disk_use'
|
||||
arg: '/home'
|
||||
|
||||
sensor 2:
|
||||
platform: forecast
|
||||
api_key: <register on Forecast.io for your PRIVATE API>
|
||||
monitored_conditions:
|
||||
- summary
|
||||
- precip_type
|
||||
- precip_intensity
|
||||
- temperature
|
||||
platform: cpuspeed
|
||||
|
||||
script:
|
||||
# Turns on the bedroom lights and then the living room lights 1 minute later
|
||||
wakeup:
|
||||
alias: Wake Up
|
||||
sequence:
|
||||
# alias is optional
|
||||
- event: LOGBOOK_ENTRY
|
||||
event_data:
|
||||
name: Paulus
|
||||
message: is waking up
|
||||
entity_id: device_tracker.paulus
|
||||
domain: light
|
||||
- alias: Bedroom lights on
|
||||
execute_service: light.turn_on
|
||||
service_data:
|
||||
service: light.turn_on
|
||||
data:
|
||||
entity_id: group.bedroom
|
||||
brightness: 100
|
||||
- delay:
|
||||
# supports seconds, milliseconds, minutes, hours, etc.
|
||||
minutes: 1
|
||||
- alias: Living room lights on
|
||||
execute_service: light.turn_on
|
||||
service_data:
|
||||
service: light.turn_on
|
||||
data:
|
||||
entity_id: group.living_room
|
||||
|
||||
scene:
|
||||
|
||||
@@ -8,11 +8,31 @@
|
||||
.. autoclass:: Config
|
||||
:members:
|
||||
|
||||
.. autoclass:: Event
|
||||
:members:
|
||||
|
||||
.. autoclass:: EventBus
|
||||
:members:
|
||||
|
||||
.. autoclass:: HomeAssistant
|
||||
:members:
|
||||
|
||||
.. autoclass:: State
|
||||
:members:
|
||||
|
||||
.. autoclass:: StateMachine
|
||||
:members:
|
||||
|
||||
.. autoclass:: ServiceCall
|
||||
:members:
|
||||
|
||||
.. autoclass:: ServiceRegistry
|
||||
:members:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: homeassistant.core
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -4,6 +4,14 @@ homeassistant.util package
|
||||
Submodules
|
||||
----------
|
||||
|
||||
homeassistant.util.async module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: homeassistant.util.async
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
homeassistant.util.color module
|
||||
-------------------------------
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ swagger: '2.0'
|
||||
info:
|
||||
title: Home Assistant
|
||||
description: Home Assistant REST API
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
# the domain of the service
|
||||
host: localhost:8123
|
||||
|
||||
@@ -12,17 +12,17 @@ schemes:
|
||||
- https
|
||||
|
||||
securityDefinitions:
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: api_password
|
||||
in: query
|
||||
|
||||
# api_key:
|
||||
#api_key:
|
||||
# type: apiKey
|
||||
# description: API password
|
||||
# name: x-ha-access
|
||||
# in: header
|
||||
# name: api_password
|
||||
# in: query
|
||||
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: x-ha-access
|
||||
in: header
|
||||
|
||||
# will be prefixed to all paths
|
||||
basePath: /api
|
||||
@@ -38,6 +38,8 @@ paths:
|
||||
description: Returns message if API is up and running.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: API is up and running
|
||||
@@ -53,6 +55,8 @@ paths:
|
||||
description: Returns the current configuration as JSON.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Current configuration
|
||||
@@ -81,6 +85,8 @@ paths:
|
||||
summary: Returns all data needed to bootstrap Home Assistant.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Bootstrap information
|
||||
@@ -96,6 +102,8 @@ paths:
|
||||
description: Returns an array of event objects. Each event object contain event name and listener count.
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Events
|
||||
@@ -113,6 +121,8 @@ paths:
|
||||
description: Returns an array of service objects. Each object contains the domain and which services it contains.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Services
|
||||
@@ -130,6 +140,8 @@ paths:
|
||||
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: State changes
|
||||
@@ -148,6 +160,8 @@ paths:
|
||||
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: States
|
||||
@@ -166,6 +180,8 @@ paths:
|
||||
Returns a state object for specified entity_id.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
@@ -223,6 +239,8 @@ paths:
|
||||
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
@@ -239,6 +257,8 @@ paths:
|
||||
Returns the data (image) from the specified camera entity_id.
|
||||
tags:
|
||||
- Camera
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- image/jpeg
|
||||
parameters:
|
||||
@@ -262,6 +282,8 @@ paths:
|
||||
Fires an event with event_type
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
@@ -286,6 +308,8 @@ paths:
|
||||
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
@@ -317,6 +341,8 @@ paths:
|
||||
Render a Home Assistant template.
|
||||
tags:
|
||||
- Template
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
@@ -338,6 +364,8 @@ paths:
|
||||
Setup event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
@@ -376,6 +404,8 @@ paths:
|
||||
tags:
|
||||
- Core
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/event-stream
|
||||
parameters:
|
||||
@@ -420,8 +450,16 @@ definitions:
|
||||
location_name:
|
||||
type: string
|
||||
unit_system:
|
||||
type: string
|
||||
description: The system for measurement units
|
||||
type: object
|
||||
properties:
|
||||
length:
|
||||
type: string
|
||||
mass:
|
||||
type: string
|
||||
temperature:
|
||||
type: string
|
||||
volume:
|
||||
type: string
|
||||
time_zone:
|
||||
type: string
|
||||
version:
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
__version__,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
REQUIRED_PYTHON_VER,
|
||||
REQUIRED_PYTHON_VER_WIN,
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
@@ -44,8 +45,7 @@ def monkey_patch_asyncio():
|
||||
See https://bugs.python.org/issue26617 for details of the Python
|
||||
bug.
|
||||
"""
|
||||
# pylint: disable=no-self-use, too-few-public-methods, protected-access
|
||||
# pylint: disable=bare-except
|
||||
# pylint: disable=no-self-use, protected-access, bare-except
|
||||
import asyncio.tasks
|
||||
|
||||
class IgnoreCalls:
|
||||
@@ -64,7 +64,12 @@ def monkey_patch_asyncio():
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate we're running the right Python version."""
|
||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
if sys.platform == "win32" and \
|
||||
sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER_WIN))
|
||||
sys.exit(1)
|
||||
elif sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER))
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Provides methods to bootstrap a home assistant instance."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from threading import RLock
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Any, Optional, Dict
|
||||
@@ -19,6 +18,8 @@ import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.util.async import (
|
||||
run_coroutine_threadsafe, run_callback_threadsafe)
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -26,21 +27,34 @@ from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SETUP_LOCK = RLock()
|
||||
_CURRENT_SETUP = []
|
||||
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
_PERSISTENT_ERRORS = {}
|
||||
HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)'
|
||||
|
||||
|
||||
def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_setup_component(hass, domain, config), loop=hass.loop).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if domain in hass.config.components:
|
||||
_LOGGER.debug('Component %s already set up.', domain)
|
||||
return True
|
||||
|
||||
_ensure_loader_prepared(hass)
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
if config is None:
|
||||
config = defaultdict(dict)
|
||||
@@ -49,10 +63,14 @@ def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
|
||||
# OrderedSet is empty if component or dependencies could not be resolved
|
||||
if not components:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
for component in components:
|
||||
if not _setup_component(hass, component, config):
|
||||
res = yield from _async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error('Component %s failed to setup', component)
|
||||
_async_persistent_notification(hass, component, True)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -60,7 +78,10 @@ def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
|
||||
def _handle_requirements(hass: core.HomeAssistant, component,
|
||||
name: str) -> bool:
|
||||
"""Install the requirements for a component."""
|
||||
"""Install the requirements for a component.
|
||||
|
||||
This method needs to run in an executor.
|
||||
"""
|
||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||
return True
|
||||
|
||||
@@ -68,70 +89,117 @@ def _handle_requirements(hass: core.HomeAssistant, component,
|
||||
if not pkg_util.install_package(req, target=hass.config.path('deps')):
|
||||
_LOGGER.error('Not initializing %s because could not install '
|
||||
'dependency %s', name, req)
|
||||
_async_persistent_notification(hass, name)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
||||
"""Setup a component for Home Assistant."""
|
||||
# pylint: disable=too-many-return-statements,too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
@asyncio.coroutine
|
||||
def _async_setup_component(hass: core.HomeAssistant,
|
||||
domain: str, config) -> bool:
|
||||
"""Setup a component for Home Assistant.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
|
||||
with _SETUP_LOCK:
|
||||
# It might have been loaded while waiting for lock
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
if domain in _CURRENT_SETUP:
|
||||
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
||||
domain, domain)
|
||||
return False
|
||||
setup_progress = hass.data.get('setup_progress')
|
||||
if setup_progress is None:
|
||||
setup_progress = hass.data['setup_progress'] = []
|
||||
|
||||
config = prepare_setup_component(hass, config, domain)
|
||||
if domain in setup_progress:
|
||||
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
||||
domain, domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Used to indicate to discovery that a setup is ongoing and allow it
|
||||
# to wait till it is done.
|
||||
did_lock = False
|
||||
if not setup_lock.locked():
|
||||
yield from setup_lock.acquire()
|
||||
did_lock = True
|
||||
|
||||
setup_progress.append(domain)
|
||||
config = yield from async_prepare_setup_component(hass, config, domain)
|
||||
|
||||
if config is None:
|
||||
return False
|
||||
|
||||
component = loader.get_component(domain)
|
||||
_CURRENT_SETUP.append(domain)
|
||||
if component is None:
|
||||
_async_persistent_notification(hass, domain)
|
||||
return False
|
||||
|
||||
async_comp = hasattr(component, 'async_setup')
|
||||
|
||||
try:
|
||||
result = component.setup(hass, config)
|
||||
if result is False:
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
return False
|
||||
elif result is not True:
|
||||
_LOGGER.error('component %s did not return boolean if setup '
|
||||
'was successful. Disabling component.', domain)
|
||||
loader.set_component(domain, None)
|
||||
return False
|
||||
if async_comp:
|
||||
result = yield from component.async_setup(hass, config)
|
||||
else:
|
||||
result = yield from hass.loop.run_in_executor(
|
||||
None, component.setup, hass, config)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error during setup of component %s', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
if result is False:
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
elif result is not True:
|
||||
_LOGGER.error('component %s did not return boolean if setup '
|
||||
'was successful. Disabling component.', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
loader.set_component(domain, None)
|
||||
return False
|
||||
finally:
|
||||
_CURRENT_SETUP.remove(domain)
|
||||
|
||||
hass.config.components.append(component.DOMAIN)
|
||||
|
||||
# Assumption: if a component does not depend on groups
|
||||
# it communicates with devices
|
||||
if 'group' not in getattr(component, 'DEPENDENCIES', []) and \
|
||||
hass.pool.worker_count <= 10:
|
||||
hass.pool.add_worker()
|
||||
if (not async_comp and
|
||||
'group' not in getattr(component, 'DEPENDENCIES', [])):
|
||||
if hass.pool is None:
|
||||
hass.async_init_pool()
|
||||
if hass.pool.worker_count <= 10:
|
||||
hass.pool.add_worker()
|
||||
|
||||
hass.bus.fire(
|
||||
hass.bus.async_fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||
)
|
||||
|
||||
return True
|
||||
finally:
|
||||
setup_progress.remove(domain)
|
||||
if did_lock:
|
||||
setup_lock.release()
|
||||
|
||||
|
||||
def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_component(hass, config, domain), loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
component = loader.get_component(domain)
|
||||
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
||||
@@ -147,7 +215,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, domain, config)
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
return None
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
@@ -157,8 +225,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, domain, config)
|
||||
return None
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
continue
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
# So if p_name is None we are not going to validate platform
|
||||
@@ -167,20 +235,21 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
platforms.append(p_validated)
|
||||
continue
|
||||
|
||||
platform = prepare_setup_platform(hass, config, domain,
|
||||
p_name)
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, domain, p_name)
|
||||
|
||||
if platform is None:
|
||||
return None
|
||||
continue
|
||||
|
||||
# Validate platform specific schema
|
||||
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated)
|
||||
return None
|
||||
async_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated, hass)
|
||||
continue
|
||||
|
||||
platforms.append(p_validated)
|
||||
|
||||
@@ -191,7 +260,9 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
if key not in filter_keys}
|
||||
config[domain] = platforms
|
||||
|
||||
if not _handle_requirements(hass, component, domain):
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, component, domain)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return config
|
||||
@@ -200,7 +271,22 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) -> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup."""
|
||||
_ensure_loader_prepared(hass)
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_platform(hass, config, domain, platform_name),
|
||||
loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) \
|
||||
-> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
|
||||
|
||||
@@ -209,6 +295,7 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
# Not found
|
||||
if platform is None:
|
||||
_LOGGER.error('Unable to find platform %s', platform_path)
|
||||
_async_persistent_notification(hass, platform_path)
|
||||
return None
|
||||
|
||||
# Already loaded
|
||||
@@ -217,20 +304,23 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
|
||||
# Load dependencies
|
||||
for component in getattr(platform, 'DEPENDENCIES', []):
|
||||
if not setup_component(hass, component, config):
|
||||
res = yield from async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error(
|
||||
'Unable to prepare setup for platform %s because '
|
||||
'dependency %s could not be initialized', platform_path,
|
||||
component)
|
||||
_async_persistent_notification(hass, platform_path, True)
|
||||
return None
|
||||
|
||||
if not _handle_requirements(hass, platform, platform_path):
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, platform, platform_path)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return platform
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
hass: Optional[core.HomeAssistant]=None,
|
||||
config_dir: Optional[str]=None,
|
||||
@@ -250,15 +340,49 @@ def from_config_dict(config: Dict[str, Any],
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_dict(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.loop.create_task(_async_init_from_config_dict(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
|
||||
return future.result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_from_config_dict(config: Dict[str, Any],
|
||||
hass: core.HomeAssistant,
|
||||
config_dir: Optional[str]=None,
|
||||
enable_log: bool=True,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=False,
|
||||
log_rotate_days: Any=None) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a config dict.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
conf_util.process_ha_core_config(hass, core_config)
|
||||
yield from conf_util.async_process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, 'homeassistant', core_config)
|
||||
async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
conf_util.process_ha_config_upgrade(hass)
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
if enable_log:
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
@@ -268,7 +392,8 @@ def from_config_dict(config: Dict[str, Any],
|
||||
_LOGGER.warning('Skipping pip installation of required modules. '
|
||||
'This may cause issues.')
|
||||
|
||||
_ensure_loader_prepared(hass)
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
# Convert it to defaultdict so components can always have config dict
|
||||
@@ -280,29 +405,26 @@ def from_config_dict(config: Dict[str, Any],
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
|
||||
# Setup in a thread to avoid blocking
|
||||
def component_setup():
|
||||
"""Set up a component."""
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
return hass
|
||||
# setup components
|
||||
# pylint: disable=not-an-iterable
|
||||
res = yield from core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
return hass
|
||||
|
||||
persistent_notification.setup(hass, config)
|
||||
yield from persistent_notification.async_setup(hass, config)
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
|
||||
# Setup the components
|
||||
for domain in loader.load_order_components(components):
|
||||
_setup_component(hass, domain, config)
|
||||
# Setup the components
|
||||
for domain in loader.load_order_components(components):
|
||||
yield from _async_setup_component(hass, domain, config)
|
||||
|
||||
hass.loop.run_until_complete(
|
||||
hass.loop.run_in_executor(None, component_setup)
|
||||
)
|
||||
return hass
|
||||
|
||||
|
||||
@@ -319,30 +441,71 @@ def from_config_file(config_path: str,
|
||||
if hass is None:
|
||||
hass = core.HomeAssistant()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_file(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.loop.create_task(_async_init_from_config_file(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
|
||||
return future.result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_from_config_file(config_path: str,
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=True,
|
||||
log_rotate_days: Any=None):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, mount_local_lib_path, config_dir)
|
||||
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
try:
|
||||
config_dict = conf_util.load_yaml_config_file(config_path)
|
||||
config_dict = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError:
|
||||
return None
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
|
||||
return from_config_dict(config_dict, hass, enable_log=False,
|
||||
skip_pip=skip_pip)
|
||||
hass = yield from async_from_config_dict(
|
||||
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
||||
return hass
|
||||
|
||||
|
||||
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
log_rotate_days=None) -> None:
|
||||
"""Setup the logging."""
|
||||
"""Setup the logging.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
"[%(name)s] %(message)s%(reset)s")
|
||||
|
||||
# suppress overly verbose logs from libraries that aren't helpful
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
@@ -389,36 +552,62 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
'Unable to setup error log %s (access denied)', err_log_path)
|
||||
|
||||
|
||||
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
||||
"""Ensure Home Assistant loader is prepared."""
|
||||
if not loader.PREPARED:
|
||||
loader.prepare(hass)
|
||||
|
||||
|
||||
def log_exception(ex, domain, config):
|
||||
def log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_log_exception, ex, domain, config, hass).result()
|
||||
|
||||
|
||||
@core.callback
|
||||
def _async_persistent_notification(hass: core.HomeAssistant, component: str,
|
||||
link: Optional[bool]=False):
|
||||
"""Print a persistent notification.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
_PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link
|
||||
_lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name)
|
||||
if link else name for name, link in _PERSISTENT_ERRORS.items()]
|
||||
message = ('The following components and platforms could not be set up:\n'
|
||||
'* ' + '\n* '.join(list(_lst)) + '\nPlease check your config')
|
||||
persistent_notification.async_create(
|
||||
hass, message, 'Invalid config', 'invalid_config')
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
if hass is not None:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||
.format(ex.path[-1], domain, domain,
|
||||
'->'.join('%s' % m for m in ex.path))
|
||||
'->'.join(str(m) for m in ex.path))
|
||||
else:
|
||||
message += '{}.'.format(humanize_error(config, ex))
|
||||
|
||||
if hasattr(config, '__line__'):
|
||||
message += " (See {}:{})".format(
|
||||
config.__config_file__, config.__line__ or '?')
|
||||
domain_config = config.get(domain, config)
|
||||
message += " (See {}:{}). ".format(
|
||||
getattr(domain_config, '__config_file__', '?'),
|
||||
getattr(domain_config, '__line__', '?'))
|
||||
|
||||
if domain != 'homeassistant':
|
||||
message += (' Please check the docs at '
|
||||
message += ('Please check the docs at '
|
||||
'https://home-assistant.io/components/{}/'.format(domain))
|
||||
|
||||
_LOGGER.error(message)
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path."""
|
||||
"""Add local library to Python Path.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
if deps_dir not in sys.path:
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
|
||||
@@ -7,6 +7,7 @@ Component design guidelines:
|
||||
format "<DOMAIN>.<OBJECT_ID>".
|
||||
- Each component should publish services only under its own domain.
|
||||
"""
|
||||
import asyncio
|
||||
import itertools as it
|
||||
import logging
|
||||
|
||||
@@ -79,8 +80,10 @@ def reload_core_config(hass):
|
||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup general services related to Home Assistant."""
|
||||
@asyncio.coroutine
|
||||
def handle_turn_service(service):
|
||||
"""Method to handle calls to homeassistant.turn_on/off."""
|
||||
entity_ids = extract_entity_ids(hass, service)
|
||||
@@ -96,6 +99,8 @@ def setup(hass, config):
|
||||
by_domain = it.groupby(sorted(entity_ids),
|
||||
lambda item: ha.split_entity_id(item)[0])
|
||||
|
||||
tasks = []
|
||||
|
||||
for domain, ent_ids in by_domain:
|
||||
# We want to block for all calls and only return when all calls
|
||||
# have been processed. If a service does not exist it causes a 10
|
||||
@@ -111,27 +116,34 @@ def setup(hass, config):
|
||||
# ent_ids is a generator, convert it to a list.
|
||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||
|
||||
hass.services.call(domain, service.service, data, blocking)
|
||||
tasks.append(hass.services.async_call(
|
||||
domain, service.service, data, blocking))
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import config as conf_util
|
||||
|
||||
try:
|
||||
path = conf_util.find_config_file(hass.config.config_dir)
|
||||
conf = conf_util.load_yaml_config_file(path)
|
||||
conf = yield from conf_util.async_hass_config_yaml(hass)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
|
||||
yield from conf_util.async_process_ha_core_config(
|
||||
hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
|
||||
handle_reload_config)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -39,11 +39,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
add_devices([AlarmDotCom(hass, name, code, username, password)])
|
||||
add_devices([AlarmDotCom(hass, name, code, username, password)], True)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
"""Represent an Alarm.com status."""
|
||||
|
||||
@@ -56,12 +54,17 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Fetch the latest state."""
|
||||
self._state = self._alarm.state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alarm."""
|
||||
@@ -75,11 +78,11 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._alarm.state == 'Disarmed':
|
||||
if self._state == 'Disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
elif self._alarm.state == 'Armed Stay':
|
||||
elif self._state == 'Armed Stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state == 'Armed Away':
|
||||
elif self._state == 'Armed Away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
132
homeassistant/components/alarm_control_panel/concord232.py
Executable file
132
homeassistant/components/alarm_control_panel/concord232.py
Executable file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Support for Concord232 alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.concord232/
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Concord232 alarm control panel platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
|
||||
try:
|
||||
add_devices([Concord232Alarm(hass, url, name)])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return False
|
||||
|
||||
|
||||
class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
"""Represents the Concord232-based alarm panel."""
|
||||
|
||||
def __init__(self, hass, url, name):
|
||||
"""Initialize the Concord232 alarm panel."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._url = url
|
||||
|
||||
try:
|
||||
client = concord232_client.Client(self._url)
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
|
||||
self._alarm = client
|
||||
self._alarm.partitions = self._alarm.list_partitions()
|
||||
self._alarm.last_partition_update = datetime.datetime.now()
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling needed."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update values from API."""
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex))
|
||||
newstate = STATE_UNKNOWN
|
||||
except IndexError:
|
||||
_LOGGER.error("Concord232 reports no partitions")
|
||||
newstate = STATE_UNKNOWN
|
||||
|
||||
if part['arming_level'] == 'Off':
|
||||
newstate = STATE_ALARM_DISARMED
|
||||
elif 'Home' in part['arming_level']:
|
||||
newstate = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
newstate = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
if not newstate == self._state:
|
||||
_LOGGER.info("State Chnage from %s to %s", self._state, newstate)
|
||||
self._state = newstate
|
||||
return self._state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._alarm.disarm(code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('home')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('auto')
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command."""
|
||||
raise NotImplementedError()
|
||||
@@ -5,25 +5,22 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
EnvisalinkDevice,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_CODE,
|
||||
CONF_PANIC,
|
||||
CONF_PARTITIONNAME,
|
||||
SIGNAL_PARTITION_UPDATE,
|
||||
SIGNAL_KEYPAD_UPDATE)
|
||||
from homeassistant.components.envisalink import (
|
||||
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
_code = discovery_info[CONF_CODE]
|
||||
@@ -38,30 +35,29 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
_panic_type,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
add_devices([_device])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Represents the Envisalink-based alarm panel."""
|
||||
"""Representation of an Envisalink-based alarm panel."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, partition_number, alarm_name,
|
||||
code, panic_type, info, controller):
|
||||
def __init__(self, partition_number, alarm_name, code, panic_type, info,
|
||||
controller):
|
||||
"""Initialize the alarm panel."""
|
||||
from pydispatch import dispatcher
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
self._panic_type = panic_type
|
||||
_LOGGER.debug('Setting up alarm: ' + alarm_name)
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(
|
||||
self._update_callback, signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(
|
||||
self._update_callback, signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
def _update_callback(self, partition):
|
||||
"""Update HA state, if needed."""
|
||||
@@ -90,20 +86,20 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
EVL_CONTROLLER.disarm_partition(
|
||||
str(code), self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
EVL_CONTROLLER.arm_stay_partition(
|
||||
str(code), self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
EVL_CONTROLLER.arm_away_partition(
|
||||
str(code), self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
|
||||
@@ -50,8 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class ManualAlarm(alarm.AlarmControlPanel):
|
||||
"""
|
||||
Represents an alarm status.
|
||||
|
||||
@@ -55,8 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
config.get(CONF_CODE))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class MqttAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
# talk to the API and trigger a requests exception for setup_platform()
|
||||
# to catch
|
||||
self._alarm.list_zones()
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -79,16 +80,20 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Process new events from panel."""
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
zones = self._alarm.list_zones()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||
dict(host=self._url, reason=ex))
|
||||
return STATE_UNKNOWN
|
||||
self._state = STATE_UNKNOWN
|
||||
except IndexError:
|
||||
_LOGGER.error('nx584 reports no partitions')
|
||||
return STATE_UNKNOWN
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
bypassed = False
|
||||
for zone in zones:
|
||||
@@ -100,11 +105,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
break
|
||||
|
||||
if not part['armed']:
|
||||
return STATE_ALARM_DISARMED
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif bypassed:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([SimpliSafeAlarm(name, username, password, code)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation a SimpliSafe alarm."""
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(alarms)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
"""Represent a Verisure alarm status."""
|
||||
|
||||
|
||||
@@ -4,19 +4,25 @@ 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 copy
|
||||
import enum
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_ENDPOINT = '/api/alexa'
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
|
||||
|
||||
CONF_ACTION = 'action'
|
||||
CONF_CARD = 'card'
|
||||
@@ -28,6 +34,23 @@ CONF_TITLE = 'title'
|
||||
CONF_CONTENT = 'content'
|
||||
CONF_TEXT = 'text'
|
||||
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
CONF_UID = 'uid'
|
||||
CONF_DATE = 'date'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_AUDIO = 'audio'
|
||||
CONF_TEXT = 'text'
|
||||
CONF_DISPLAY_URL = 'display_url'
|
||||
|
||||
ATTR_UID = 'uid'
|
||||
ATTR_UPDATE_DATE = 'updateDate'
|
||||
ATTR_TITLE_TEXT = 'titleText'
|
||||
ATTR_STREAM_URL = 'streamUrl'
|
||||
ATTR_MAIN_TEXT = 'mainText'
|
||||
ATTR_REDIRECTION_URL = 'redirectionURL'
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
@@ -61,6 +84,16 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TEXT): cv.template,
|
||||
}
|
||||
}
|
||||
},
|
||||
CONF_FLASH_BRIEFINGS: {
|
||||
cv.string: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
|
||||
vol.Optional(CONF_DATE, default=datetime.utcnow()): cv.string,
|
||||
vol.Required(CONF_TITLE): cv.template,
|
||||
vol.Optional(CONF_AUDIO): cv.template,
|
||||
vol.Required(CONF_TEXT, default=""): cv.template,
|
||||
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
||||
}]),
|
||||
}
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@@ -68,16 +101,19 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
def setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
hass.wsgi.register_view(AlexaView(hass,
|
||||
config[DOMAIN].get(CONF_INTENTS, {})))
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
|
||||
|
||||
hass.http.register_view(AlexaIntentsView(hass, intents))
|
||||
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlexaView(HomeAssistantView):
|
||||
class AlexaIntentsView(HomeAssistantView):
|
||||
"""Handle Alexa requests."""
|
||||
|
||||
url = API_ENDPOINT
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:alexa'
|
||||
|
||||
def __init__(self, hass, intents):
|
||||
@@ -94,9 +130,10 @@ class AlexaView(HomeAssistantView):
|
||||
|
||||
self.intents = intents
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
data = request.json
|
||||
data = yield from request.json()
|
||||
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
|
||||
@@ -142,7 +179,7 @@ class AlexaView(HomeAssistantView):
|
||||
action = config.get(CONF_ACTION)
|
||||
|
||||
if action is not None:
|
||||
action.run(response.variables)
|
||||
yield from action.async_run(response.variables)
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
@@ -184,8 +221,8 @@ class AlexaResponse(object):
|
||||
self.card = card
|
||||
return
|
||||
|
||||
card["title"] = title.render(self.variables)
|
||||
card["content"] = content.render(self.variables)
|
||||
card["title"] = title.async_render(self.variables)
|
||||
card["content"] = content.async_render(self.variables)
|
||||
self.card = card
|
||||
|
||||
def add_speech(self, speech_type, text):
|
||||
@@ -195,7 +232,7 @@ class AlexaResponse(object):
|
||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||
|
||||
if isinstance(text, template.Template):
|
||||
text = text.render(self.variables)
|
||||
text = text.async_render(self.variables)
|
||||
|
||||
self.speech = {
|
||||
'type': speech_type.value,
|
||||
@@ -210,7 +247,7 @@ class AlexaResponse(object):
|
||||
|
||||
self.reprompt = {
|
||||
'type': speech_type.value,
|
||||
key: text.render(self.variables)
|
||||
key: text.async_render(self.variables)
|
||||
}
|
||||
|
||||
def as_dict(self):
|
||||
@@ -235,3 +272,69 @@ class AlexaResponse(object):
|
||||
'sessionAttributes': self.session_attributes,
|
||||
'response': response,
|
||||
}
|
||||
|
||||
|
||||
class AlexaFlashBriefingView(HomeAssistantView):
|
||||
"""Handle Alexa Flash Briefing skill requests."""
|
||||
|
||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||
name = 'api:alexa:flash_briefings'
|
||||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
@callback
|
||||
def get(self, request, briefing_id):
|
||||
"""Handle Alexa Flash Briefing request."""
|
||||
_LOGGER.debug('Received Alexa flash briefing request for: %s',
|
||||
briefing_id)
|
||||
|
||||
if self.flash_briefings.get(briefing_id) is None:
|
||||
err = 'No configured Alexa flash briefing was found for: %s'
|
||||
_LOGGER.error(err, briefing_id)
|
||||
return b'', 404
|
||||
|
||||
briefing = []
|
||||
|
||||
for item in self.flash_briefings.get(briefing_id, []):
|
||||
output = {}
|
||||
if item.get(CONF_TITLE) is not None:
|
||||
if isinstance(item.get(CONF_TITLE), template.Template):
|
||||
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render()
|
||||
else:
|
||||
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
|
||||
|
||||
if item.get(CONF_TEXT) is not None:
|
||||
if isinstance(item.get(CONF_TEXT), template.Template):
|
||||
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render()
|
||||
else:
|
||||
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
|
||||
|
||||
if item.get(CONF_UID) is not None:
|
||||
output[ATTR_UID] = item.get(CONF_UID)
|
||||
|
||||
if item.get(CONF_AUDIO) is not None:
|
||||
if isinstance(item.get(CONF_AUDIO), template.Template):
|
||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render()
|
||||
else:
|
||||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||
|
||||
if item.get(CONF_DISPLAY_URL) is not None:
|
||||
if isinstance(item.get(CONF_DISPLAY_URL),
|
||||
template.Template):
|
||||
output[ATTR_REDIRECTION_URL] = \
|
||||
item[CONF_DISPLAY_URL].async_render()
|
||||
else:
|
||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||
|
||||
if isinstance(item[CONF_DATE], str):
|
||||
item[CONF_DATE] = dt_util.parse_datetime(item[CONF_DATE])
|
||||
|
||||
output[ATTR_UPDATE_DATE] = item[CONF_DATE].strftime(DATE_FORMAT)
|
||||
|
||||
briefing.append(output)
|
||||
|
||||
return self.json(briefing)
|
||||
|
||||
@@ -7,7 +7,9 @@ https://home-assistant.io/developers/api/
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
@@ -21,7 +23,7 @@ from homeassistant.const import (
|
||||
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import TrackStates
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
@@ -36,20 +38,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup(hass, config):
|
||||
"""Register the API with the HTTP interface."""
|
||||
hass.wsgi.register_view(APIStatusView)
|
||||
hass.wsgi.register_view(APIEventStream)
|
||||
hass.wsgi.register_view(APIConfigView)
|
||||
hass.wsgi.register_view(APIDiscoveryView)
|
||||
hass.wsgi.register_view(APIStatesView)
|
||||
hass.wsgi.register_view(APIEntityStateView)
|
||||
hass.wsgi.register_view(APIEventListenersView)
|
||||
hass.wsgi.register_view(APIEventView)
|
||||
hass.wsgi.register_view(APIServicesView)
|
||||
hass.wsgi.register_view(APIDomainServicesView)
|
||||
hass.wsgi.register_view(APIEventForwardingView)
|
||||
hass.wsgi.register_view(APIComponentsView)
|
||||
hass.wsgi.register_view(APIErrorLogView)
|
||||
hass.wsgi.register_view(APITemplateView)
|
||||
hass.http.register_view(APIStatusView)
|
||||
hass.http.register_view(APIEventStream)
|
||||
hass.http.register_view(APIConfigView)
|
||||
hass.http.register_view(APIDiscoveryView)
|
||||
hass.http.register_view(APIStatesView)
|
||||
hass.http.register_view(APIEntityStateView)
|
||||
hass.http.register_view(APIEventListenersView)
|
||||
hass.http.register_view(APIEventView)
|
||||
hass.http.register_view(APIServicesView)
|
||||
hass.http.register_view(APIDomainServicesView)
|
||||
hass.http.register_view(APIEventForwardingView)
|
||||
hass.http.register_view(APIComponentsView)
|
||||
hass.http.register_view(APIErrorLogView)
|
||||
hass.http.register_view(APITemplateView)
|
||||
|
||||
return True
|
||||
|
||||
@@ -60,6 +62,7 @@ class APIStatusView(HomeAssistantView):
|
||||
url = URL_API
|
||||
name = "api:status"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Retrieve if API is running."""
|
||||
return self.json_message('API running.')
|
||||
@@ -71,12 +74,13 @@ class APIEventStream(HomeAssistantView):
|
||||
url = URL_API_STREAM
|
||||
name = "api:stream"
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
stop_obj = object()
|
||||
to_write = queue.Queue()
|
||||
to_write = asyncio.Queue(loop=self.hass.loop)
|
||||
|
||||
restrict = request.args.get('restrict')
|
||||
restrict = request.GET.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||
|
||||
@@ -96,38 +100,40 @@ class APIEventStream(HomeAssistantView):
|
||||
else:
|
||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||
|
||||
to_write.put(data)
|
||||
yield from to_write.put(data)
|
||||
|
||||
def stream():
|
||||
"""Stream events to response."""
|
||||
unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events)
|
||||
response = web.StreamResponse()
|
||||
response.content_type = 'text/event-stream'
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||
|
||||
# Fire off one message so browsers fire open event right away
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
try:
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
|
||||
while True:
|
||||
try:
|
||||
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
|
||||
# Fire off one message so browsers fire open event right away
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
if payload is stop_obj:
|
||||
break
|
||||
while True:
|
||||
try:
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||
loop=self.hass.loop):
|
||||
payload = yield from to_write.get()
|
||||
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
yield msg.encode("UTF-8")
|
||||
except queue.Empty:
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
except GeneratorExit:
|
||||
if payload is stop_obj:
|
||||
break
|
||||
finally:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
unsub_stream()
|
||||
|
||||
return self.Response(stream(), mimetype='text/event-stream')
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
response.write(msg.encode("UTF-8"))
|
||||
yield from response.drain()
|
||||
except asyncio.TimeoutError:
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
finally:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
unsub_stream()
|
||||
|
||||
|
||||
class APIConfigView(HomeAssistantView):
|
||||
@@ -136,6 +142,7 @@ class APIConfigView(HomeAssistantView):
|
||||
url = URL_API_CONFIG
|
||||
name = "api:config"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current configuration."""
|
||||
return self.json(self.hass.config.as_dict())
|
||||
@@ -148,6 +155,7 @@ class APIDiscoveryView(HomeAssistantView):
|
||||
url = URL_API_DISCOVERY_INFO
|
||||
name = "api:discovery"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get discovery info."""
|
||||
needs_auth = self.hass.config.api.api_password is not None
|
||||
@@ -165,17 +173,19 @@ class APIStatesView(HomeAssistantView):
|
||||
url = URL_API_STATES
|
||||
name = "api:states"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current states."""
|
||||
return self.json(self.hass.states.all())
|
||||
return self.json(self.hass.states.async_all())
|
||||
|
||||
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
"""View to handle EntityState requests."""
|
||||
|
||||
url = "/api/states/<entity(exist=False):entity_id>"
|
||||
url = "/api/states/{entity_id}"
|
||||
name = "api:entity-state"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve state of entity."""
|
||||
state = self.hass.states.get(entity_id)
|
||||
@@ -184,34 +194,41 @@ class APIEntityStateView(HomeAssistantView):
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
try:
|
||||
new_state = request.json['state']
|
||||
except KeyError:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
new_state = data.get('state')
|
||||
|
||||
if not new_state:
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = request.json.get('attributes')
|
||||
force_update = request.json.get('force_update', False)
|
||||
attributes = data.get('attributes')
|
||||
force_update = data.get('force_update', False)
|
||||
|
||||
is_new_state = self.hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
self.hass.states.set(entity_id, new_state, attributes, force_update)
|
||||
self.hass.states.async_set(entity_id, new_state, attributes,
|
||||
force_update)
|
||||
|
||||
# Read the state back for our response
|
||||
resp = self.json(self.hass.states.get(entity_id))
|
||||
|
||||
if is_new_state:
|
||||
resp.status_code = HTTP_CREATED
|
||||
status_code = HTTP_CREATED if is_new_state else 200
|
||||
resp = self.json(self.hass.states.get(entity_id), status_code)
|
||||
|
||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
return resp
|
||||
|
||||
@ha.callback
|
||||
def delete(self, request, entity_id):
|
||||
"""Remove entity."""
|
||||
if self.hass.states.remove(entity_id):
|
||||
if self.hass.states.async_remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
@@ -223,20 +240,23 @@ class APIEventListenersView(HomeAssistantView):
|
||||
url = URL_API_EVENTS
|
||||
name = "api:event-listeners"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get event listeners."""
|
||||
return self.json(events_json(self.hass))
|
||||
return self.json(async_events_json(self.hass))
|
||||
|
||||
|
||||
class APIEventView(HomeAssistantView):
|
||||
"""View to handle Event requests."""
|
||||
|
||||
url = '/api/events/<event_type>'
|
||||
url = '/api/events/{event_type}'
|
||||
name = "api:event"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
event_data = request.json
|
||||
body = yield from request.text()
|
||||
event_data = json.loads(body) if body else None
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
return self.json_message('Event data should be a JSON object',
|
||||
@@ -251,7 +271,7 @@ class APIEventView(HomeAssistantView):
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
|
||||
self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote)
|
||||
|
||||
return self.json_message("Event {} fired.".format(event_type))
|
||||
|
||||
@@ -262,24 +282,30 @@ class APIServicesView(HomeAssistantView):
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get registered services."""
|
||||
return self.json(services_json(self.hass))
|
||||
return self.json(async_services_json(self.hass))
|
||||
|
||||
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
"""View to handle DomainServices requests."""
|
||||
|
||||
url = "/api/services/<domain>/<service>"
|
||||
url = "/api/services/{domain}/{service}"
|
||||
name = "api:domain-services"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, domain, service):
|
||||
"""Call a service.
|
||||
|
||||
Returns a list of changed states.
|
||||
"""
|
||||
with TrackStates(self.hass) as changed_states:
|
||||
self.hass.services.call(domain, service, request.json, True)
|
||||
body = yield from request.text()
|
||||
data = json.loads(body) if body else None
|
||||
|
||||
with AsyncTrackStates(self.hass) as changed_states:
|
||||
yield from self.hass.services.async_call(domain, service, data,
|
||||
True)
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
@@ -291,11 +317,14 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
name = "api:event-forward"
|
||||
event_forwarder = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
data = request.json
|
||||
if data is None:
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
@@ -311,21 +340,25 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
if not api.validate_api():
|
||||
valid = yield from self.hass.loop.run_in_executor(
|
||||
None, api.validate_api)
|
||||
if not valid:
|
||||
return self.json_message("Unable to validate API.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self.event_forwarder is None:
|
||||
self.event_forwarder = rem.EventForwarder(self.hass)
|
||||
|
||||
self.event_forwarder.connect(api)
|
||||
self.event_forwarder.async_connect(api)
|
||||
|
||||
return self.json_message("Event forwarding setup.")
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete(self, request):
|
||||
"""Remove event forwarer."""
|
||||
data = request.json
|
||||
if data is None:
|
||||
"""Remove event forwarder."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
@@ -342,7 +375,7 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
if self.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
self.event_forwarder.disconnect(api)
|
||||
self.event_forwarder.async_disconnect(api)
|
||||
|
||||
return self.json_message("Event forwarding cancelled.")
|
||||
|
||||
@@ -353,6 +386,7 @@ class APIComponentsView(HomeAssistantView):
|
||||
url = URL_API_COMPONENTS
|
||||
name = "api:components"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current loaded components."""
|
||||
return self.json(self.hass.config.components)
|
||||
@@ -364,9 +398,12 @@ class APIErrorLogView(HomeAssistantView):
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error-log"
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Serve error log."""
|
||||
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
|
||||
resp = yield from self.file(
|
||||
request, self.hass.config.path(ERROR_LOG_FILENAME))
|
||||
return resp
|
||||
|
||||
|
||||
class APITemplateView(HomeAssistantView):
|
||||
@@ -375,23 +412,25 @@ class APITemplateView(HomeAssistantView):
|
||||
url = URL_API_TEMPLATE
|
||||
name = "api:template"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Render a template."""
|
||||
try:
|
||||
tpl = template.Template(request.json['template'], self.hass)
|
||||
return tpl.render(request.json.get('variables'))
|
||||
except TemplateError as ex:
|
||||
data = yield from request.json()
|
||||
tpl = template.Template(data['template'], self.hass)
|
||||
return tpl.async_render(data.get('variables'))
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message('Error rendering template: {}'.format(ex),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
def services_json(hass):
|
||||
def async_services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
return [{"domain": key, "services": value}
|
||||
for key, value in hass.services.services.items()]
|
||||
for key, value in hass.services.async_services().items()]
|
||||
|
||||
|
||||
def events_json(hass):
|
||||
def async_events_json(hass):
|
||||
"""Generate event data to JSONify."""
|
||||
return [{"event": key, "listener_count": value}
|
||||
for key, value in hass.bus.listeners.items()]
|
||||
for key, value in hass.bus.async_listeners().items()]
|
||||
|
||||
@@ -11,7 +11,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.bootstrap import async_prepare_setup_platform
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
@@ -24,7 +24,6 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.loader import get_platform
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@@ -143,68 +142,75 @@ def reload(hass):
|
||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the automation."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
||||
|
||||
success = run_coroutine_threadsafe(
|
||||
_async_process_config(hass, config, component), hass.loop).result()
|
||||
success = yield from _async_process_config(hass, config, component)
|
||||
|
||||
if not success:
|
||||
return False
|
||||
|
||||
descriptions = conf_util.load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def trigger_service_handler(service_call):
|
||||
"""Handle automation triggers."""
|
||||
for entity in component.extract_from_service(service_call):
|
||||
hass.loop.create_task(entity.async_trigger(
|
||||
tasks = []
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES), True))
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def turn_onoff_service_handler(service_call):
|
||||
"""Handle automation turn on/off service calls."""
|
||||
tasks = []
|
||||
method = 'async_{}'.format(service_call.service)
|
||||
for entity in component.extract_from_service(service_call):
|
||||
hass.loop.create_task(getattr(entity, method)())
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(getattr(entity, method)())
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def toggle_service_handler(service_call):
|
||||
"""Handle automation toggle service calls."""
|
||||
for entity in component.extract_from_service(service_call):
|
||||
tasks = []
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
if entity.is_on:
|
||||
hass.loop.create_task(entity.async_turn_off())
|
||||
tasks.append(entity.async_turn_off())
|
||||
else:
|
||||
hass.loop.create_task(entity.async_turn_on())
|
||||
tasks.append(entity.async_turn_on())
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
"""Remove all automations and load new ones from config."""
|
||||
conf = yield from hass.loop.run_in_executor(
|
||||
None, component.prepare_reload)
|
||||
conf = yield from component.async_prepare_reload()
|
||||
if conf is None:
|
||||
return
|
||||
hass.loop.create_task(_async_process_config(hass, conf, component))
|
||||
yield from _async_process_config(hass, conf, component)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
descriptions.get(SERVICE_TRIGGER),
|
||||
schema=TRIGGER_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions.get(SERVICE_RELOAD),
|
||||
schema=RELOAD_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||
descriptions.get(SERVICE_TOGGLE),
|
||||
schema=SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||
descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA)
|
||||
|
||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
||||
hass.services.register(DOMAIN, service, turn_onoff_service_handler,
|
||||
descriptions.get(service),
|
||||
schema=SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, turn_onoff_service_handler,
|
||||
descriptions.get(service), schema=SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -212,8 +218,6 @@ def setup(hass, config):
|
||||
class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||
hidden):
|
||||
"""Initialize an automation entity."""
|
||||
@@ -260,7 +264,7 @@ class AutomationEntity(ToggleEntity):
|
||||
return
|
||||
|
||||
yield from self.async_enable()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs) -> None:
|
||||
@@ -271,7 +275,7 @@ class AutomationEntity(ToggleEntity):
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
self._enabled = False
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(self, variables, skip_condition=False):
|
||||
@@ -280,15 +284,15 @@ class AutomationEntity(ToggleEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if skip_condition or self._cond_func(variables):
|
||||
yield from self._async_action(variables)
|
||||
yield from self._async_action(self.entity_id, variables)
|
||||
self._last_triggered = utcnow()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
def remove(self):
|
||||
@asyncio.coroutine
|
||||
def async_remove(self):
|
||||
"""Remove automation from HASS."""
|
||||
run_coroutine_threadsafe(self.async_turn_off(),
|
||||
self.hass.loop).result()
|
||||
super().remove()
|
||||
yield from self.async_turn_off()
|
||||
yield from super().async_remove()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_enable(self):
|
||||
@@ -341,12 +345,11 @@ def _async_process_config(hass, config, component):
|
||||
entity = AutomationEntity(name, async_attach_triggers, cond_func,
|
||||
action, hidden)
|
||||
if config_block[CONF_INITIAL_STATE]:
|
||||
tasks.append(hass.loop.create_task(entity.async_enable()))
|
||||
tasks.append(entity.async_enable())
|
||||
entities.append(entity)
|
||||
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, component.add_entities, entities)
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
return len(entities) > 0
|
||||
|
||||
@@ -356,11 +359,12 @@ def _async_get_action(hass, config, name):
|
||||
script_obj = script.Script(hass, config, name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def action(variables=None):
|
||||
def action(entity_id, variables):
|
||||
"""Action to be executed."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.async_log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
hass.loop.create_task(script_obj.async_run(variables))
|
||||
logbook.async_log_entry(
|
||||
hass, name, 'has been triggered', DOMAIN, entity_id)
|
||||
yield from script_obj.async_run(variables)
|
||||
|
||||
return action
|
||||
|
||||
@@ -393,9 +397,8 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
removes = []
|
||||
|
||||
for conf in trigger_configs:
|
||||
platform = yield from hass.loop.run_in_executor(
|
||||
None, prepare_setup_platform, hass, config, DOMAIN,
|
||||
conf.get(CONF_PLATFORM))
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
|
||||
|
||||
if platform is None:
|
||||
return None
|
||||
|
||||
41
homeassistant/components/automation/litejet.py
Normal file
41
homeassistant/components/automation/litejet.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Trigger an automation when a LiteJet switch is released.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/automation.litejet/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['litejet']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NUMBER = 'number'
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'litejet',
|
||||
vol.Required(CONF_NUMBER): cv.positive_int
|
||||
})
|
||||
|
||||
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
number = config.get(CONF_NUMBER)
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
CONF_PLATFORM: 'litejet',
|
||||
CONF_NUMBER: number
|
||||
},
|
||||
})
|
||||
|
||||
hass.data['litejet_system'].on_switch_released(number, call_action)
|
||||
@@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
sensor_class, pin)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
"""Implement an aREST binary sensor for a pin."""
|
||||
|
||||
@@ -93,7 +92,6 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
self.arest.update()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ArestData(object):
|
||||
"""Class for handling the data retrieval for pins."""
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
value_template)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class CommandBinarySensor(BinarySensorDevice):
|
||||
"""Represent a command line binary sensor."""
|
||||
|
||||
|
||||
136
homeassistant/components/binary_sensor/concord232.py
Executable file
136
homeassistant/components/binary_sensor/concord232.py
Executable file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Support for exposing Concord232 elements as sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.concord232/
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_EXCLUDE_ZONES = 'exclude_zones'
|
||||
CONF_ZONE_TYPES = 'zone_types'
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'Alarm'
|
||||
DEFAULT_PORT = '5007'
|
||||
DEFAULT_SSL = False
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_EXCLUDE_ZONES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Concord232 binary sensor platform."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
exclude = config.get(CONF_EXCLUDE_ZONES)
|
||||
zone_types = config.get(CONF_ZONE_TYPES)
|
||||
sensors = []
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Initializing Client")
|
||||
client = concord232_client.Client('http://{}:{}'.format(host, port))
|
||||
client.zones = client.list_zones()
|
||||
client.last_zone_update = datetime.datetime.now()
|
||||
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return False
|
||||
|
||||
for zone in client.zones:
|
||||
_LOGGER.info("Loading Zone found: %s", zone['name'])
|
||||
if zone['number'] not in exclude:
|
||||
sensors.append(
|
||||
Concord232ZoneSensor(
|
||||
hass, client, zone, zone_types.get(zone['number'],
|
||||
get_opening_type(zone)))
|
||||
)
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_opening_type(zone):
|
||||
"""Helper function to try to guess sensor type from name."""
|
||||
if 'MOTION' in zone['name']:
|
||||
return 'motion'
|
||||
if 'KEY' in zone['name']:
|
||||
return 'safety'
|
||||
if 'SMOKE' in zone['name']:
|
||||
return 'smoke'
|
||||
if 'WATER' in zone['name']:
|
||||
return 'water'
|
||||
return 'opening'
|
||||
|
||||
|
||||
class Concord232ZoneSensor(BinarySensorDevice):
|
||||
"""Representation of a Concord232 zone as a sensor."""
|
||||
|
||||
def __init__(self, hass, client, zone, zone_type):
|
||||
"""Initialize the Concord232 binary sensor."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._zone = zone
|
||||
self._number = zone['number']
|
||||
self._zone_type = zone_type
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._zone['name']
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
# True means "faulted" or "open" or "abnormal state"
|
||||
return bool(self._zone['state'] == 'Normal')
|
||||
|
||||
def update(self):
|
||||
""""Get updated stats from API."""
|
||||
last_update = datetime.datetime.now() - self._client.last_zone_update
|
||||
_LOGGER.debug("Zone: %s ", self._zone)
|
||||
if last_update > datetime.timedelta(seconds=1):
|
||||
self._client.zones = self._client.list_zones()
|
||||
self._client.last_zone_update = datetime.datetime.now()
|
||||
_LOGGER.debug("Updated from Zone: %s", self._zone['name'])
|
||||
|
||||
if hasattr(self._client, 'zones'):
|
||||
self._zone = next((x for x in self._client.zones
|
||||
if x['number'] == self._number), None)
|
||||
@@ -35,7 +35,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""Representation of an Envisalink binary sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
|
||||
@@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
from haffmpeg import SensorNoise, SensorMotion
|
||||
|
||||
# check source
|
||||
if not run_test(config.get(CONF_INPUT)):
|
||||
if not run_test(hass, config.get(CONF_INPUT)):
|
||||
return
|
||||
|
||||
# generate sensor object
|
||||
|
||||
@@ -51,7 +51,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttBinarySensor(BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
|
||||
import homeassistant.components.nest as nest
|
||||
from homeassistant.components.nest import DATA_NEST
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
@@ -35,9 +35,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Nest binary sensors."""
|
||||
nest = hass.data[DATA_NEST]
|
||||
|
||||
all_sensors = []
|
||||
for structure, device in nest.devices():
|
||||
add_devices([NestBinarySensor(structure, device, variable)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]])
|
||||
all_sensors.extend(
|
||||
[NestBinarySensor(structure, device, variable)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]])
|
||||
|
||||
add_devices(all_sensors, True)
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
@@ -46,4 +52,8 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the binary sensor is on."""
|
||||
return bool(getattr(self.device, self.variable))
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = bool(getattr(self.device, self.variable))
|
||||
|
||||
127
homeassistant/components/binary_sensor/netatmo.py
Normal file
127
homeassistant/components/binary_sensor/netatmo.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Support for the Netatmo binary sensors.
|
||||
|
||||
The binary sensors based on events seen by the NetatmoCamera
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.netatmo/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.netatmo import WelcomeData
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
"Someone known": "motion",
|
||||
"Someone unknown": "motion",
|
||||
"Motion": "motion",
|
||||
}
|
||||
|
||||
CONF_HOME = 'home'
|
||||
CONF_CAMERAS = 'cameras'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup access to Netatmo binary sensor."""
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME, None)
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
if data.get_camera_names() == []:
|
||||
return None
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES)
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
for variable in sensors:
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, home,
|
||||
variable)])
|
||||
|
||||
|
||||
class WelcomeBinarySensor(BinarySensorDevice):
|
||||
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
||||
|
||||
def __init__(self, data, camera_name, home, sensor):
|
||||
"""Setup for access to the Netatmo camera events."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._home = home
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
self._sensor_name = sensor
|
||||
self._name += ' ' + sensor
|
||||
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
||||
home=home)['id']
|
||||
self._unique_id = "Welcome_binary_sensor {0} - {1}".format(self._name,
|
||||
camera_id)
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the Netatmo device and this sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Request an update from the Netatmo API."""
|
||||
self._data.update()
|
||||
self._data.welcomedata.updateEvent(home=self._data.home)
|
||||
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneKnownSeen(self._home,
|
||||
self._camera_name)
|
||||
elif self._sensor_name == "Someone unknown":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneUnknownSeen(self._home,
|
||||
self._camera_name)
|
||||
elif self._sensor_name == "Motion":
|
||||
self._state =\
|
||||
self._data.welcomedata.motionDetected(self._home,
|
||||
self._camera_name)
|
||||
else:
|
||||
return None
|
||||
@@ -58,11 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class OctoPrintBinarySensor(BinarySensorDevice):
|
||||
"""Representation an OctoPrint binary sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, api, condition, sensor_type, sensor_name, unit,
|
||||
endpoint, group, tool=None):
|
||||
"""Initialize a new OctoPrint sensor."""
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
@@ -30,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_AUTHENTICATION):
|
||||
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
|
||||
vol.Optional(CONF_HEADERS): cv.string,
|
||||
vol.Optional(CONF_HEADERS): {cv.string: cv.string},
|
||||
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
@@ -42,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-variable, too-many-locals
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the REST binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
@@ -52,7 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
headers = json.loads(config.get(CONF_HEADERS, '{}'))
|
||||
headers = config.get(CONF_HEADERS)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
@@ -70,14 +68,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
rest.update()
|
||||
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch REST data')
|
||||
_LOGGER.error("Unable to fetch REST data from %s", resource)
|
||||
return False
|
||||
|
||||
add_devices([RestBinarySensor(
|
||||
hass, rest, name, sensor_class, value_template)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a REST binary sensor."""
|
||||
|
||||
@@ -109,8 +106,8 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
if self._value_template is not None:
|
||||
response = self._value_template.render_with_possible_json_value(
|
||||
self.rest.data, False)
|
||||
response = self._value_template.\
|
||||
async_render_with_possible_json_value(self.rest.data, False)
|
||||
|
||||
try:
|
||||
return bool(int(response))
|
||||
|
||||
@@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(binary_sensors)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
"""Represent a binary sensor that uses Raspberry Pi GPIO."""
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
|
||||
"""Implementation of a SleepIQ presence sensor."""
|
||||
|
||||
|
||||
@@ -7,21 +7,20 @@ https://home-assistant.io/components/binary_sensor.tcp/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.tcp import Sensor, CONF_VALUE_ON
|
||||
|
||||
from homeassistant.components.sensor.tcp import (
|
||||
TcpSensor, CONF_VALUE_ON, PLATFORM_SCHEMA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Create the binary sensor."""
|
||||
if not BinarySensor.validate_config(config):
|
||||
return False
|
||||
|
||||
add_entities((BinarySensor(hass, config),))
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
|
||||
|
||||
|
||||
class BinarySensor(BinarySensorDevice, Sensor):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the TCP binary sensor."""
|
||||
add_devices([TcpBinarySensor(hass, config)])
|
||||
|
||||
|
||||
class TcpBinarySensor(BinarySensorDevice, TcpSensor):
|
||||
"""A binary sensor which is on when its state == CONF_VALUE_ON."""
|
||||
|
||||
required = (CONF_VALUE_ON,)
|
||||
|
||||
@@ -17,8 +17,8 @@ from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,7 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup template binary sensors."""
|
||||
sensors = []
|
||||
|
||||
@@ -61,34 +62,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if not sensors:
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
add_devices(sensors)
|
||||
|
||||
hass.loop.create_task(async_add_devices(sensors, True))
|
||||
return True
|
||||
|
||||
|
||||
class BinarySensorTemplate(BinarySensorDevice):
|
||||
"""A virtual binary sensor that triggers from another sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, device, friendly_name, sensor_class,
|
||||
value_template, entity_ids):
|
||||
"""Initialize the Template binary sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
hass=hass)
|
||||
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
hass=hass)
|
||||
self._name = friendly_name
|
||||
self._sensor_class = sensor_class
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
|
||||
self.update()
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
hass.loop.create_task(self.async_update_ha_state(True))
|
||||
|
||||
track_state_change(hass, entity_ids, template_bsensor_state_listener)
|
||||
async_track_state_change(
|
||||
hass, entity_ids, template_bsensor_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -112,7 +111,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
"""Update the state from the template."""
|
||||
try:
|
||||
self._state = self._template.async_render().lower() == 'true'
|
||||
except TemplateError as ex:
|
||||
|
||||
@@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class SensorTrend(BinarySensorDevice):
|
||||
"""Representation of a trend Sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
def __init__(self, hass, device_id, friendly_name,
|
||||
target_entity, attribute, sensor_class, invert):
|
||||
"""Initialize the sensor."""
|
||||
|
||||
@@ -16,9 +16,9 @@ DEPENDENCIES = ['vera']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Vera controller devices."""
|
||||
add_devices_callback(
|
||||
add_devices(
|
||||
VeraBinarySensor(device, VERA_CONTROLLER)
|
||||
for device in VERA_DEVICES['binary_sensor'])
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument,too-few-public-methods
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Setup BloomSky component."""
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
|
||||
@@ -5,8 +5,10 @@ Component to interface with cameras.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -25,17 +27,16 @@ STATE_IDLE = 'idle'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the camera component."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.wsgi.register_view(CameraImageView(hass, component.entities))
|
||||
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
|
||||
|
||||
component.setup(config)
|
||||
hass.http.register_view(CameraImageView(hass, component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(hass, component.entities))
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
@@ -80,33 +81,58 @@ class Camera(Entity):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
def stream():
|
||||
"""Stream images as mjpeg stream."""
|
||||
try:
|
||||
last_image = None
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return bytes of camera image.
|
||||
|
||||
if img_bytes is not None and img_bytes != last_image:
|
||||
yield bytes(
|
||||
'--jpegboundary\r\n'
|
||||
'Content-Type: image/jpeg\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n'
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
image = yield from self.hass.loop.run_in_executor(
|
||||
None, self.camera_image)
|
||||
return image
|
||||
|
||||
last_image = img_bytes
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from camera images.
|
||||
|
||||
time.sleep(0.5)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
response = web.StreamResponse()
|
||||
|
||||
return response(
|
||||
stream(),
|
||||
content_type=('multipart/x-mixed-replace; '
|
||||
'boundary=--jpegboundary')
|
||||
)
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--jpegboundary')
|
||||
yield from response.prepare(request)
|
||||
|
||||
def write(img_bytes):
|
||||
"""Write image to stream."""
|
||||
response.write(bytes(
|
||||
'--jpegboundary\r\n'
|
||||
'Content-Type: image/jpeg\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
|
||||
|
||||
last_image = None
|
||||
|
||||
try:
|
||||
while True:
|
||||
img_bytes = yield from self.async_camera_image()
|
||||
if not img_bytes:
|
||||
break
|
||||
|
||||
if img_bytes is not None and img_bytes != last_image:
|
||||
write(img_bytes)
|
||||
|
||||
# Chrome seems to always ignore first picture,
|
||||
# print it twice.
|
||||
if last_image is None:
|
||||
write(img_bytes)
|
||||
|
||||
last_image = img_bytes
|
||||
yield from response.drain()
|
||||
|
||||
yield from asyncio.sleep(.5)
|
||||
finally:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -144,22 +170,25 @@ class CameraView(HomeAssistantView):
|
||||
super().__init__(hass)
|
||||
self.entities = entities
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, entity_id):
|
||||
"""Start a get request."""
|
||||
camera = self.entities.get(entity_id)
|
||||
|
||||
if camera is None:
|
||||
return self.Response(status=404)
|
||||
return web.Response(status=404)
|
||||
|
||||
authenticated = (request.authenticated or
|
||||
request.args.get('token') == camera.access_token)
|
||||
request.GET.get('token') == camera.access_token)
|
||||
|
||||
if not authenticated:
|
||||
return self.Response(status=401)
|
||||
return web.Response(status=401)
|
||||
|
||||
return self.handle(camera)
|
||||
response = yield from self.handle(request, camera)
|
||||
return response
|
||||
|
||||
def handle(self, camera):
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Hanlde the camera request."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -167,25 +196,27 @@ class CameraView(HomeAssistantView):
|
||||
class CameraImageView(CameraView):
|
||||
"""Camera view to serve an image."""
|
||||
|
||||
url = "/api/camera_proxy/<entity(domain=camera):entity_id>"
|
||||
url = "/api/camera_proxy/{entity_id}"
|
||||
name = "api:camera:image"
|
||||
|
||||
def handle(self, camera):
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
response = camera.camera_image()
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
if response is None:
|
||||
return self.Response(status=500)
|
||||
if image is None:
|
||||
return web.Response(status=500)
|
||||
|
||||
return self.Response(response)
|
||||
return web.Response(body=image)
|
||||
|
||||
|
||||
class CameraMjpegStream(CameraView):
|
||||
"""Camera View to serve an MJPEG stream."""
|
||||
|
||||
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>"
|
||||
url = "/api/camera_proxy_stream/{entity_id}"
|
||||
name = "api:camera:stream"
|
||||
|
||||
def handle(self, camera):
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
return camera.mjpeg_stream(self.Response)
|
||||
yield from camera.handle_async_mjpeg_stream(request)
|
||||
|
||||
@@ -4,15 +4,18 @@ Support for Cameras with FFmpeg as decoder.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.ffmpeg/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import (
|
||||
run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
||||
async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
@@ -27,17 +30,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
if not run_test(config.get(CONF_INPUT)):
|
||||
if not async_run_test(hass, config.get(CONF_INPUT)):
|
||||
return
|
||||
add_devices([FFmpegCamera(config)])
|
||||
hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)]))
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
"""An implementation of an FFmpeg camera."""
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a FFmpeg camera."""
|
||||
super().__init__()
|
||||
self._name = config.get(CONF_NAME)
|
||||
@@ -45,24 +49,44 @@ class FFmpegCamera(Camera):
|
||||
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageSingle, IMAGE_JPEG
|
||||
ffmpeg = ImageSingle(get_binary())
|
||||
from haffmpeg import ImageSingleAsync, IMAGE_JPEG
|
||||
ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop)
|
||||
|
||||
return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments)
|
||||
image = yield from ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments)
|
||||
return image
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
from haffmpeg import CameraMjpegAsync
|
||||
|
||||
stream = CameraMjpeg(get_binary())
|
||||
stream.open_camera(self._input, extra_cmd=self._extra_arguments)
|
||||
return response(
|
||||
stream,
|
||||
mimetype='multipart/x-mixed-replace;boundary=ffserver',
|
||||
direct_passthrough=True
|
||||
)
|
||||
stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
self._input, extra_cmd=self._extra_arguments)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = 'multipart/x-mixed-replace;boundary=ffserver'
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
finally:
|
||||
self.hass.loop.create_task(stream.close())
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([FoscamCamera(config)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class FoscamCamera(Camera):
|
||||
"""An implementation of a Foscam IP camera."""
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@ Support for IP Cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.generic/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
from requests.auth import HTTPDigestAuth
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -16,6 +19,7 @@ from homeassistant.const import (
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,13 +39,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a generic IP Camera."""
|
||||
add_devices([GenericCamera(hass, config)])
|
||||
hass.loop.create_task(async_add_devices([GenericCamera(hass, config)]))
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GenericCamera(Camera):
|
||||
"""A generic implementation of an IP camera."""
|
||||
|
||||
@@ -49,6 +53,7 @@ class GenericCamera(Camera):
|
||||
"""Initialize a generic camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._authentication = device_info.get(CONF_AUTHENTICATION)
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
||||
self._still_image_url.hass = hass
|
||||
@@ -58,10 +63,10 @@ class GenericCamera(Camera):
|
||||
password = device_info.get(CONF_PASSWORD)
|
||||
|
||||
if username and password:
|
||||
if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION:
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
self._auth = HTTPDigestAuth(username, password)
|
||||
else:
|
||||
self._auth = HTTPBasicAuth(username, password)
|
||||
self._auth = aiohttp.BasicAuth(username, password=password)
|
||||
else:
|
||||
self._auth = None
|
||||
|
||||
@@ -69,9 +74,15 @@ class GenericCamera(Camera):
|
||||
self._last_image = None
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
try:
|
||||
url = self._still_image_url.render()
|
||||
url = self._still_image_url.async_render()
|
||||
except TemplateError as err:
|
||||
_LOGGER.error('Error parsing template %s: %s',
|
||||
self._still_image_url, err)
|
||||
@@ -80,16 +91,37 @@ class GenericCamera(Camera):
|
||||
if url == self._last_url and self._limit_refetch:
|
||||
return self._last_image
|
||||
|
||||
kwargs = {'timeout': 10, 'auth': self._auth}
|
||||
# aiohttp don't support DigestAuth yet
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
def fetch():
|
||||
"""Read image from a URL."""
|
||||
try:
|
||||
kwargs = {'timeout': 10, 'auth': self._auth}
|
||||
response = requests.get(url, **kwargs)
|
||||
return response.content
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return self._last_image
|
||||
|
||||
try:
|
||||
response = requests.get(url, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return None
|
||||
self._last_image = yield from self.hass.loop.run_in_executor(
|
||||
None, fetch)
|
||||
# async
|
||||
else:
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.hass.websession.get(
|
||||
url, auth=self._auth)
|
||||
self._last_image = yield from response.read()
|
||||
yield from response.release()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error('Timeout getting camera image')
|
||||
return self._last_image
|
||||
except (aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError) as err:
|
||||
_LOGGER.error('Error getting new camera image: %s', err)
|
||||
return self._last_image
|
||||
|
||||
self._last_url = url
|
||||
self._last_image = response.content
|
||||
return self._last_image
|
||||
|
||||
@property
|
||||
|
||||
@@ -4,9 +4,14 @@ Support for IP Cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.mjpeg/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import closing
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
import voluptuous as vol
|
||||
@@ -34,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a MJPEG IP Camera."""
|
||||
add_devices([MjpegCamera(config)])
|
||||
hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)]))
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
@@ -52,11 +58,10 @@ def extract_image_from_mjpeg(stream):
|
||||
return jpg
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class MjpegCamera(Camera):
|
||||
"""An implementation of an IP camera that is reachable over a URL."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
def __init__(self, hass, device_info):
|
||||
"""Initialize a MJPEG camera."""
|
||||
super().__init__()
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
@@ -65,32 +70,60 @@ class MjpegCamera(Camera):
|
||||
self._password = device_info.get(CONF_PASSWORD)
|
||||
self._mjpeg_url = device_info[CONF_MJPEG_URL]
|
||||
|
||||
def camera_stream(self):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
self._auth = None
|
||||
if self._username and self._password:
|
||||
if self._authentication == HTTP_BASIC_AUTHENTICATION:
|
||||
self._auth = aiohttp.BasicAuth(
|
||||
self._username, password=self._password
|
||||
)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
if self._username and self._password:
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
auth = HTTPDigestAuth(self._username, self._password)
|
||||
else:
|
||||
auth = HTTPBasicAuth(self._username, self._password)
|
||||
return requests.get(self._mjpeg_url,
|
||||
auth=auth,
|
||||
stream=True, timeout=10)
|
||||
req = requests.get(
|
||||
self._mjpeg_url, auth=auth, stream=True, timeout=10)
|
||||
else:
|
||||
return requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||
req = requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
with closing(self.camera_stream()) as response:
|
||||
return extract_image_from_mjpeg(response.iter_content(1024))
|
||||
with closing(req) as response:
|
||||
return extract_image_from_mjpeg(response.iter_content(102400))
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
stream = self.camera_stream()
|
||||
return response(
|
||||
stream.iter_content(chunk_size=1024),
|
||||
mimetype=stream.headers[CONTENT_TYPE_HEADER],
|
||||
direct_passthrough=True
|
||||
)
|
||||
# aiohttp don't support DigestAuth -> Fallback
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
yield from super().handle_async_mjpeg_stream(request)
|
||||
return
|
||||
|
||||
# connect to stream
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
stream = yield from self.hass.websession.get(
|
||||
self._mjpeg_url,
|
||||
auth=self._auth
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.content.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
finally:
|
||||
self.hass.loop.create_task(stream.release())
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -5,12 +5,11 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.netatmo import WelcomeData
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -22,8 +21,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_HOME = 'home'
|
||||
CONF_CAMERAS = 'cameras'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
@@ -39,15 +36,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
for camera_name in data.get_camera_names():
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
add_devices([WelcomeCamera(data, camera_name, home)])
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if config[CONF_CAMERAS] != []:
|
||||
if camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
add_devices([WelcomeCamera(data, camera_name, home)])
|
||||
|
||||
|
||||
class WelcomeCamera(Camera):
|
||||
"""Representation of the images published from Welcome camera."""
|
||||
@@ -61,6 +58,10 @@ class WelcomeCamera(Camera):
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
||||
home=home)['id']
|
||||
self._unique_id = "Welcome_camera {0} - {1}".format(self._name,
|
||||
camera_id)
|
||||
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls(
|
||||
camera=camera_name
|
||||
)
|
||||
@@ -87,31 +88,7 @@ class WelcomeCamera(Camera):
|
||||
"""Return the name of this Netatmo Welcome device."""
|
||||
return self._name
|
||||
|
||||
|
||||
class WelcomeData(object):
|
||||
"""Get the latest data from NetAtmo."""
|
||||
|
||||
def __init__(self, auth, home=None):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.welcomedata = None
|
||||
self.camera_names = []
|
||||
self.home = home
|
||||
|
||||
def get_camera_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.update()
|
||||
if not self.home:
|
||||
for home in self.welcomedata.cameras:
|
||||
for camera in self.welcomedata.cameras[home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
else:
|
||||
for camera in self.welcomedata.cameras[self.home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
return self.camera_names
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.welcomedata = lnetatmo.WelcomeData(self.auth)
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@@ -12,7 +12,8 @@ import shutil
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH)
|
||||
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,7 +36,7 @@ DEFAULT_TIMELAPSE = 1000
|
||||
DEFAULT_VERTICAL_FLIP = 0
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FILE_PATH): cv.isfile,
|
||||
vol.Optional(CONF_FILE_PATH): cv.string,
|
||||
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
||||
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP):
|
||||
@@ -53,6 +54,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def kill_raspistill(*args):
|
||||
"""Kill any previously running raspistill process.."""
|
||||
subprocess.Popen(['killall', 'raspistill'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Raspberry Camera."""
|
||||
if shutil.which("raspistill") is None:
|
||||
@@ -75,11 +83,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
}
|
||||
)
|
||||
|
||||
if not os.access(setup_config[CONF_FILE_PATH], os.W_OK):
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill)
|
||||
|
||||
try:
|
||||
# Try to create an empty file (or open existing) to ensure we have
|
||||
# proper permissions.
|
||||
open(setup_config[CONF_FILE_PATH], 'a').close()
|
||||
|
||||
add_devices([RaspberryCamera(setup_config)])
|
||||
except PermissionError:
|
||||
_LOGGER.error("File path is not writable")
|
||||
return False
|
||||
|
||||
add_devices([RaspberryCamera(setup_config)])
|
||||
except FileNotFoundError:
|
||||
_LOGGER.error("Could not create output file (missing directory?)")
|
||||
return False
|
||||
|
||||
|
||||
class RaspberryCamera(Camera):
|
||||
@@ -93,9 +110,7 @@ class RaspberryCamera(Camera):
|
||||
self._config = device_info
|
||||
|
||||
# Kill if there's raspistill instance
|
||||
subprocess.Popen(['killall', 'raspistill'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT)
|
||||
kill_raspistill()
|
||||
|
||||
cmd_args = [
|
||||
'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH],
|
||||
|
||||
293
homeassistant/components/camera/synology.py
Normal file
293
homeassistant/components/camera/synology.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Support for Synology Surveillance Station Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.synology/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
|
||||
QUERY_CGI = 'query.cgi'
|
||||
QUERY_API = 'SYNO.API.Info'
|
||||
AUTH_API = 'SYNO.API.Auth'
|
||||
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
|
||||
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
|
||||
SESSION_ID = '0'
|
||||
|
||||
WEBAPI_PATH = '/webapi/'
|
||||
AUTH_PATH = 'auth.cgi'
|
||||
CAMERA_PATH = 'camera.cgi'
|
||||
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
SYNO_API_URL = '{0}{1}{2}'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
if not config.get(CONF_VERIFY_SSL):
|
||||
connector = aiohttp.TCPConnector(verify_ssl=False)
|
||||
else:
|
||||
connector = None
|
||||
|
||||
websession_init = aiohttp.ClientSession(
|
||||
loop=hass.loop,
|
||||
connector=connector
|
||||
)
|
||||
|
||||
# Determine API to use for authentication
|
||||
syno_api_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
||||
|
||||
query_payload = {
|
||||
'api': QUERY_API,
|
||||
'method': 'Query',
|
||||
'version': '1',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_api_url)
|
||||
return False
|
||||
|
||||
query_resp = yield from query_req.json()
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
# cleanup
|
||||
yield from query_req.release()
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||
|
||||
session_id = yield from get_session_id(
|
||||
hass,
|
||||
websession_init,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url
|
||||
)
|
||||
|
||||
websession_init.detach()
|
||||
|
||||
# init websession
|
||||
websession = aiohttp.ClientSession(
|
||||
loop=hass.loop, connector=connector, cookies={'id': session_id})
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_close_websession(event):
|
||||
"""Close webssesion on shutdown."""
|
||||
yield from websession.close()
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
||||
|
||||
# Use SessionID to get cameras in system
|
||||
syno_camera_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
||||
|
||||
camera_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'List',
|
||||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
camera_req = yield from websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_camera_url)
|
||||
return False
|
||||
|
||||
camera_resp = yield from camera_req.json()
|
||||
cameras = camera_resp['data']['cameras']
|
||||
yield from camera_req.release()
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
for camera in cameras:
|
||||
if not config.get(CONF_WHITELIST):
|
||||
camera_id = camera['id']
|
||||
snapshot_path = camera['snapshot_path']
|
||||
|
||||
device = SynologyCamera(
|
||||
hass,
|
||||
websession,
|
||||
config,
|
||||
camera_id,
|
||||
camera['name'],
|
||||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_path
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, websession, username, password, login_url):
|
||||
"""Get a session id."""
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': username,
|
||||
'passwd': password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", login_url)
|
||||
return False
|
||||
|
||||
auth_resp = yield from auth_req.json()
|
||||
yield from auth_req.release()
|
||||
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
||||
def __init__(self, hass, websession, config, camera_id,
|
||||
camera_name, snapshot_path, streaming_path, camera_path,
|
||||
auth_path):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._websession = websession
|
||||
self._name = camera_name
|
||||
self._synology_url = config.get(CONF_URL)
|
||||
self._camera_name = config.get(CONF_CAMERA_NAME)
|
||||
self._stream_id = config.get(CONF_STREAM_ID)
|
||||
self._camera_id = camera_id
|
||||
self._snapshot_path = snapshot_path
|
||||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
image_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._camera_path)
|
||||
|
||||
image_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'GetSnapshot',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
response = yield from self._websession.get(
|
||||
image_url,
|
||||
params=image_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", image_url)
|
||||
return None
|
||||
|
||||
image = yield from response.read()
|
||||
yield from response.release()
|
||||
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
streaming_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
||||
|
||||
streaming_payload = {
|
||||
'api': STREAMING_API,
|
||||
'method': 'Stream',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
stream = yield from self._websession.get(
|
||||
streaming_url,
|
||||
params=streaming_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", streaming_url)
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.content.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
finally:
|
||||
self.hass.async_add_job(stream.release())
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._name
|
||||
103
homeassistant/components/camera/verisure.py
Normal file
103
homeassistant/components/camera/verisure.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Camera that loads a picture from a local file.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.verisure/
|
||||
"""
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.components.verisure import CONF_SMARTCAM
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Camera."""
|
||||
if not int(hub.config.get(CONF_SMARTCAM, 1)):
|
||||
return False
|
||||
directory_path = hass.config.config_dir
|
||||
if not os.access(directory_path, os.R_OK):
|
||||
_LOGGER.error("file path %s is not readable", directory_path)
|
||||
return False
|
||||
hub.update_smartcam()
|
||||
smartcams = []
|
||||
smartcams.extend([
|
||||
VerisureSmartcam(hass, value.deviceLabel, directory_path)
|
||||
for value in hub.smartcam_status.values()])
|
||||
add_devices(smartcams)
|
||||
|
||||
|
||||
class VerisureSmartcam(Camera):
|
||||
"""Local camera."""
|
||||
|
||||
def __init__(self, hass, device_id, directory_path):
|
||||
"""Initialize Verisure File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._device_id = device_id
|
||||
self._directory_path = directory_path
|
||||
self._image = None
|
||||
self._image_id = None
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
self.delete_image)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
self.check_imagelist()
|
||||
if not self._image:
|
||||
_LOGGER.debug('No image to display')
|
||||
return
|
||||
_LOGGER.debug('Trying to open %s', self._image)
|
||||
with open(self._image, 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
def check_imagelist(self):
|
||||
"""Check the contents of the image list."""
|
||||
hub.update_smartcam_imagelist()
|
||||
if (self._device_id not in hub.smartcam_dict or
|
||||
not hub.smartcam_dict[self._device_id]):
|
||||
return
|
||||
images = hub.smartcam_dict[self._device_id]
|
||||
new_image_id = images[0]
|
||||
_LOGGER.debug('self._device_id=%s, self._images=%s, '
|
||||
'self._new_image_id=%s', self._device_id,
|
||||
images, new_image_id)
|
||||
if (new_image_id == '-1' or
|
||||
self._image_id == new_image_id):
|
||||
_LOGGER.debug('The image is the same, or loading image_id')
|
||||
return
|
||||
_LOGGER.debug('Download new image %s', new_image_id)
|
||||
hub.my_pages.smartcam.download_image(self._device_id,
|
||||
new_image_id,
|
||||
self._directory_path)
|
||||
_LOGGER.debug('Old image_id=%s', self._image_id)
|
||||
self.delete_image(self)
|
||||
|
||||
self._image_id = new_image_id
|
||||
self._image = os.path.join(self._directory_path,
|
||||
'{}{}'.format(
|
||||
self._image_id,
|
||||
'.jpg'))
|
||||
|
||||
def delete_image(self, event):
|
||||
"""Delete an old image."""
|
||||
remove_image = os.path.join(self._directory_path,
|
||||
'{}{}'.format(
|
||||
self._image_id,
|
||||
'.jpg'))
|
||||
try:
|
||||
os.remove(remove_image)
|
||||
_LOGGER.debug('Deleting old image %s', remove_image)
|
||||
except OSError as error:
|
||||
if error.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return hub.smartcam_status[self._device_id].location
|
||||
@@ -123,7 +123,6 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def set_temperature(hass, temperature=None, entity_id=None,
|
||||
target_temp_high=None, target_temp_low=None,
|
||||
operation_mode=None):
|
||||
@@ -181,7 +180,6 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
"""Setup climate devices."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
@@ -253,7 +251,7 @@ def setup(hass, config):
|
||||
kwargs[value] = convert_temperature(
|
||||
temp,
|
||||
hass.config.units.temperature_unit,
|
||||
climate.unit_of_measurement
|
||||
climate.temperature_unit
|
||||
)
|
||||
else:
|
||||
kwargs[value] = temp
|
||||
@@ -364,11 +362,14 @@ def setup(hass, config):
|
||||
class ClimateDevice(Entity):
|
||||
"""Representation of a climate device."""
|
||||
|
||||
# pylint: disable=too-many-public-methods,no-self-use
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
return self.current_operation or STATE_UNKNOWN
|
||||
if self.current_operation:
|
||||
return self.current_operation
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@@ -398,17 +399,20 @@ class ClimateDevice(Entity):
|
||||
fan_mode = self.current_fan_mode
|
||||
if fan_mode is not None:
|
||||
data[ATTR_FAN_MODE] = fan_mode
|
||||
data[ATTR_FAN_LIST] = self.fan_list
|
||||
if self.fan_list:
|
||||
data[ATTR_FAN_LIST] = self.fan_list
|
||||
|
||||
operation_mode = self.current_operation
|
||||
if operation_mode is not None:
|
||||
data[ATTR_OPERATION_MODE] = operation_mode
|
||||
data[ATTR_OPERATION_LIST] = self.operation_list
|
||||
if self.operation_list:
|
||||
data[ATTR_OPERATION_LIST] = self.operation_list
|
||||
|
||||
swing_mode = self.current_swing_mode
|
||||
if swing_mode is not None:
|
||||
data[ATTR_SWING_MODE] = swing_mode
|
||||
data[ATTR_SWING_LIST] = self.swing_list
|
||||
if self.swing_list:
|
||||
data[ATTR_SWING_LIST] = self.swing_list
|
||||
|
||||
is_away = self.is_away_mode_on
|
||||
if is_away is not None:
|
||||
@@ -422,7 +426,12 @@ class ClimateDevice(Entity):
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
"""The unit of measurement to display."""
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""The unit of measurement used by the platform."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@@ -534,12 +543,12 @@ class ClimateDevice(Entity):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return convert_temperature(7, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return convert_temperature(35, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit)
|
||||
|
||||
@property
|
||||
def min_humidity(self):
|
||||
@@ -556,10 +565,10 @@ class ClimateDevice(Entity):
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
return temp
|
||||
|
||||
value = convert_temperature(temp, self.unit_of_measurement,
|
||||
self.hass.config.units.temperature_unit)
|
||||
value = convert_temperature(temp, self.temperature_unit,
|
||||
self.unit_of_measurement)
|
||||
|
||||
if self.hass.config.units.temperature_unit is TEMP_CELSIUS:
|
||||
if self.unit_of_measurement is TEMP_CELSIUS:
|
||||
decimal_count = 1
|
||||
else:
|
||||
# Users of fahrenheit generally expect integer units.
|
||||
|
||||
@@ -21,11 +21,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-public-methods
|
||||
class DemoClimate(ClimateDevice):
|
||||
"""Representation of a demo climate device."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, name, target_temperature, unit_of_measurement,
|
||||
away, current_temperature, current_fan_mode,
|
||||
target_humidity, current_humidity, current_swing_mode,
|
||||
@@ -59,7 +57,7 @@ class DemoClimate(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.climate import (
|
||||
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
class Thermostat(ClimateDevice):
|
||||
"""A thermostat class for Ecobee."""
|
||||
|
||||
@@ -105,12 +104,9 @@ class Thermostat(ClimateDevice):
|
||||
return self.thermostat['name']
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self.thermostat['settings']['useCelsius']:
|
||||
return TEMP_CELSIUS
|
||||
else:
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
"""
|
||||
Support for eq3 Bluetooth Smart thermostats.
|
||||
Support for eQ-3 Bluetooth Smart thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.eq3btsmart/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
|
||||
from homeassistant.util.temperature import convert
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['bluepy_devices==0.2.0']
|
||||
|
||||
CONF_MAC = 'mac'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MODE = 'mode'
|
||||
ATTR_MODE_READABLE = 'mode_readable'
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_MAC): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICES):
|
||||
vol.Schema({cv.string: DEVICE_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the eq3 BLE thermostats."""
|
||||
"""Setup the eQ-3 BLE thermostats."""
|
||||
devices = []
|
||||
|
||||
for name, device_cfg in config[CONF_DEVICES].items():
|
||||
@@ -28,16 +42,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
|
||||
# pylint: disable=import-error
|
||||
class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Representation of a EQ3 Bluetooth Smart thermostat."""
|
||||
"""Representation of a eQ-3 Bluetooth Smart thermostat."""
|
||||
|
||||
def __init__(self, _mac, _name):
|
||||
"""Initialize the thermostat."""
|
||||
from bluepy_devices.devices import eq3btsmart
|
||||
|
||||
self._name = _name
|
||||
|
||||
self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac)
|
||||
|
||||
@property
|
||||
@@ -46,7 +59,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@@ -70,8 +83,10 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {"mode": self._thermostat.mode,
|
||||
"mode_readable": self._thermostat.mode_readable}
|
||||
return {
|
||||
ATTR_MODE: self._thermostat.mode,
|
||||
ATTR_MODE_READABLE: self._thermostat.mode_readable,
|
||||
}
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
||||
@@ -62,11 +62,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
target_temp, ac_mode, min_cycle_duration)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
class GenericThermostat(ClimateDevice):
|
||||
"""Representation of a GenericThermostat device."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
|
||||
"""Initialize the thermostat."""
|
||||
@@ -100,7 +98,7 @@ class GenericThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
|
||||
@@ -1,56 +1,54 @@
|
||||
"""
|
||||
Support for the PRT Heatmiser themostats using the V3 protocol.
|
||||
|
||||
See https://github.com/andylockran/heatmiserV3 for more info on the
|
||||
heatmiserV3 module dependency.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.heatmiser/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
import voluptuous as vol
|
||||
|
||||
CONF_IPADDRESS = 'ipaddress'
|
||||
CONF_PORT = 'port'
|
||||
CONF_TSTATS = 'tstats'
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ["heatmiserV3==0.9.1"]
|
||||
REQUIREMENTS = ['heatmiserV3==0.9.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IPADDRESS = 'ipaddress'
|
||||
CONF_TSTATS = 'tstats'
|
||||
|
||||
TSTATS_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_IPADDRESS): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Required(CONF_TSTATS, default={}):
|
||||
vol.Schema({cv.string: TSTATS_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the heatmiser thermostat."""
|
||||
from heatmiserV3 import heatmiser, connection
|
||||
|
||||
ipaddress = str(config[CONF_IPADDRESS])
|
||||
port = str(config[CONF_PORT])
|
||||
|
||||
if ipaddress is None or port is None:
|
||||
_LOGGER.error("Missing required configuration items %s or %s",
|
||||
CONF_IPADDRESS, CONF_PORT)
|
||||
return False
|
||||
ipaddress = config.get(CONF_IPADDRESS)
|
||||
port = str(config.get(CONF_PORT))
|
||||
tstats = config.get(CONF_TSTATS)
|
||||
|
||||
serport = connection.connection(ipaddress, port)
|
||||
serport.open()
|
||||
|
||||
tstats = []
|
||||
if CONF_TSTATS in config:
|
||||
tstats = config[CONF_TSTATS]
|
||||
|
||||
if tstats is None:
|
||||
_LOGGER.error("No thermostats configured.")
|
||||
return False
|
||||
|
||||
for tstat in tstats:
|
||||
for thermostat, tstat in tstats.items():
|
||||
add_devices([
|
||||
HeatmiserV3Thermostat(
|
||||
heatmiser,
|
||||
tstat.get("id"),
|
||||
tstat.get("name"),
|
||||
serport)
|
||||
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
|
||||
])
|
||||
return
|
||||
|
||||
@@ -58,7 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class HeatmiserV3Thermostat(ClimateDevice):
|
||||
"""Representation of a HeatmiserV3 thermostat."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
def __init__(self, heatmiser, device, name, serport):
|
||||
"""Initialize the thermostat."""
|
||||
self.heatmiser = heatmiser
|
||||
@@ -69,7 +66,7 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
self._id = device
|
||||
self.dcb = None
|
||||
self.update()
|
||||
self._target_temperature = int(self.dcb.get("roomset"))
|
||||
self._target_temperature = int(self.dcb.get('roomset'))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -77,7 +74,7 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@@ -85,9 +82,9 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if self.dcb is not None:
|
||||
low = self.dcb.get("floortemplow ")
|
||||
high = self.dcb.get("floortemphigh")
|
||||
temp = (high*256 + low)/10.0
|
||||
low = self.dcb.get('floortemplow ')
|
||||
high = self.dcb.get('floortemphigh')
|
||||
temp = (high * 256 + low) / 10.0
|
||||
self._current_temperature = temp
|
||||
else:
|
||||
self._current_temperature = None
|
||||
|
||||
@@ -36,12 +36,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||
"""Representation of a Homematic thermostat."""
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@ def _setup_us(username, password, config, add_devices):
|
||||
class RoundThermostat(ClimateDevice):
|
||||
"""Representation of a Honeywell Round Connected thermostat."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
def __init__(self, device, zone_id, master, away_temp):
|
||||
"""Initialize the thermostat."""
|
||||
self.device = device
|
||||
@@ -120,7 +119,7 @@ class RoundThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@@ -197,7 +196,6 @@ class RoundThermostat(ClimateDevice):
|
||||
self._is_dhw = False
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Representation of a Honeywell US Thermostat."""
|
||||
|
||||
@@ -217,7 +215,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return (TEMP_CELSIUS if self._device.temperature_unit == 'C'
|
||||
else TEMP_FAHRENHEIT)
|
||||
@@ -225,7 +223,6 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
self._device.refresh()
|
||||
return self._device.current_temperature
|
||||
|
||||
@property
|
||||
@@ -276,3 +273,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Set the system mode (Cool, Heat, etc)."""
|
||||
if hasattr(self._device, ATTR_SYSTEM_MODE):
|
||||
self._device.system_mode = operation_mode
|
||||
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
self._device.refresh()
|
||||
|
||||
@@ -56,6 +56,8 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
|
||||
self._away = False # not yet supported
|
||||
self._is_fan_on = False # not yet supported
|
||||
self._current_temp = None
|
||||
self._target_temp = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -63,23 +65,19 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
return True
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
return knx2_to_float(self.value('temperature'))
|
||||
return self._current_temp
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
return knx2_to_float(self.value('setpoint'))
|
||||
return self._target_temp
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -94,3 +92,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def update(self):
|
||||
"""Update KNX climate."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
super().update()
|
||||
|
||||
self._current_temp = knx2_to_float(self.value('temperature'))
|
||||
self._target_temp = knx2_to_float(self.value('setpoint'))
|
||||
|
||||
@@ -37,7 +37,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
map_sv_types, devices, add_devices, MySensorsHVAC))
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-public-methods
|
||||
class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
"""Representation of a MySensorsHVAC hvac."""
|
||||
|
||||
@@ -47,7 +46,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
return self.gateway.optimistic
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return (TEMP_CELSIUS
|
||||
if self.gateway.metric else TEMP_FAHRENHEIT)
|
||||
|
||||
@@ -5,8 +5,10 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.nest/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.components.nest as nest
|
||||
|
||||
from homeassistant.components.nest import DATA_NEST
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
@@ -26,11 +28,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Nest thermostat."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
add_devices([NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in nest.devices()])
|
||||
add_devices(
|
||||
[NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in hass.data[DATA_NEST].devices()],
|
||||
True
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method,too-many-public-methods
|
||||
class NestThermostat(ClimateDevice):
|
||||
"""Representation of a Nest thermostat."""
|
||||
|
||||
@@ -40,51 +44,81 @@ class NestThermostat(ClimateDevice):
|
||||
self.structure = structure
|
||||
self.device = device
|
||||
self._fan_list = [STATE_ON, STATE_AUTO]
|
||||
self._operation_list = [STATE_HEAT, STATE_COOL, STATE_AUTO,
|
||||
STATE_OFF]
|
||||
|
||||
# Not all nest devices support cooling and heating remove unused
|
||||
self._operation_list = [STATE_OFF]
|
||||
|
||||
# Add supported nest thermostat features
|
||||
if self.device.can_heat:
|
||||
self._operation_list.append(STATE_HEAT)
|
||||
|
||||
if self.device.can_cool:
|
||||
self._operation_list.append(STATE_COOL)
|
||||
|
||||
if self.device.can_heat and self.device.can_cool:
|
||||
self._operation_list.append(STATE_AUTO)
|
||||
|
||||
# feature of device
|
||||
self._has_humidifier = self.device.has_humidifier
|
||||
self._has_dehumidifier = self.device.has_dehumidifier
|
||||
self._has_fan = self.device.has_fan
|
||||
|
||||
# data attributes
|
||||
self._away = None
|
||||
self._location = None
|
||||
self._name = None
|
||||
self._humidity = None
|
||||
self._target_humidity = None
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._mode = None
|
||||
self._fan = None
|
||||
self._away_temperature = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
location = self.device.where
|
||||
name = self.device.name
|
||||
if location is None:
|
||||
return name
|
||||
if self._location is None:
|
||||
return self._name
|
||||
else:
|
||||
if name == '':
|
||||
return location.capitalize()
|
||||
if self._name == '':
|
||||
return self._location.capitalize()
|
||||
else:
|
||||
return location.capitalize() + '(' + name + ')'
|
||||
return self._location.capitalize() + '(' + self._name + ')'
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
# Move these to Thermostat Device and make them global
|
||||
return {
|
||||
"humidity": self.device.humidity,
|
||||
"target_humidity": self.device.target_humidity,
|
||||
}
|
||||
if self._has_humidifier or self._has_dehumidifier:
|
||||
# Move these to Thermostat Device and make them global
|
||||
return {
|
||||
"humidity": self._humidity,
|
||||
"target_humidity": self._target_humidity,
|
||||
}
|
||||
else:
|
||||
# No way to control humidity not show setting
|
||||
return {}
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.device.temperature
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self.device.mode == 'cool':
|
||||
if self._mode == 'cool':
|
||||
return STATE_COOL
|
||||
elif self.device.mode == 'heat':
|
||||
elif self._mode == 'heat':
|
||||
return STATE_HEAT
|
||||
elif self.device.mode == 'range':
|
||||
elif self._mode == 'range':
|
||||
return STATE_AUTO
|
||||
elif self.device.mode == 'off':
|
||||
elif self._mode == 'off':
|
||||
return STATE_OFF
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
@@ -92,37 +126,37 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.device.mode != 'range' and not self.is_away_mode_on:
|
||||
return self.device.target
|
||||
if self._mode != 'range' and not self.is_away_mode_on:
|
||||
return self._target_temperature
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.is_away_mode_on and self.device.away_temperature[0]:
|
||||
if self.is_away_mode_on and self._away_temperature[0]:
|
||||
# away_temperature is always a low, high tuple
|
||||
return self.device.away_temperature[0]
|
||||
if self.device.mode == 'range':
|
||||
return self.device.target[0]
|
||||
return self._away_temperature[0]
|
||||
if self._mode == 'range':
|
||||
return self._target_temperature[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.is_away_mode_on and self.device.away_temperature[1]:
|
||||
if self.is_away_mode_on and self._away_temperature[1]:
|
||||
# away_temperature is always a low, high tuple
|
||||
return self.device.away_temperature[1]
|
||||
if self.device.mode == 'range':
|
||||
return self.device.target[1]
|
||||
return self._away_temperature[1]
|
||||
if self._mode == 'range':
|
||||
return self._target_temperature[1]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if away mode is on."""
|
||||
return self.structure.away
|
||||
return self._away
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -130,7 +164,7 @@ class NestThermostat(ClimateDevice):
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
|
||||
if self.device.mode == 'range':
|
||||
if self._mode == 'range':
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -164,7 +198,12 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
return STATE_ON if self.device.fan else STATE_AUTO
|
||||
if self._has_fan:
|
||||
# Return whether the fan is on
|
||||
return STATE_ON if self._fan else STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
@@ -178,7 +217,7 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Identify min_temp in Nest API or defaults if not available."""
|
||||
temp = self.device.away_temperature.low
|
||||
temp = self._away_temperature[0]
|
||||
if temp is None:
|
||||
return super().min_temp
|
||||
else:
|
||||
@@ -187,12 +226,21 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Identify max_temp in Nest API or defaults if not available."""
|
||||
temp = self.device.away_temperature.high
|
||||
temp = self._away_temperature[1]
|
||||
if temp is None:
|
||||
return super().max_temp
|
||||
else:
|
||||
return temp
|
||||
|
||||
def update(self):
|
||||
"""Python-nest has its own mechanism for staying up to date."""
|
||||
pass
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._humidity = self.device.humidity,
|
||||
self._target_humidity = self.device.target_humidity,
|
||||
self._temperature = self.device.temperature
|
||||
self._mode = self.device.mode
|
||||
self._target_temperature = self.device.target
|
||||
self._fan = self.device.fan
|
||||
self._away = self.structure.away
|
||||
self._away_temperature = self.device.away_temperature
|
||||
|
||||
177
homeassistant/components/climate/netatmo.py
Executable file
177
homeassistant/components/climate/netatmo.py
Executable file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Support for Netatmo Smart Thermostat.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['netatmo']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RELAY = 'relay'
|
||||
CONF_THERMOSTAT = 'thermostat'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE = 14
|
||||
# # The default offeset is 2 hours (when you use the thermostat itself)
|
||||
DEFAULT_TIME_OFFSET = 7200
|
||||
# # Return cached results if last scan was less then this time ago
|
||||
# # NetAtmo Data is uploaded to server every hour
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_RELAY): cv.string,
|
||||
vol.Optional(CONF_THERMOSTAT, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the NetAtmo Thermostat."""
|
||||
netatmo = get_component('netatmo')
|
||||
device = config.get(CONF_RELAY)
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
data = ThermostatData(netatmo.NETATMO_AUTH, device)
|
||||
for module_name in data.get_module_names():
|
||||
if CONF_THERMOSTAT in config:
|
||||
if config[CONF_THERMOSTAT] != [] and \
|
||||
module_name not in config[CONF_THERMOSTAT]:
|
||||
continue
|
||||
add_callback_devices([NetatmoThermostat(data, module_name)])
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
|
||||
class NetatmoThermostat(ClimateDevice):
|
||||
"""Representation a Netatmo thermostat."""
|
||||
|
||||
def __init__(self, data, module_name, away_temp=None):
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self._state = None
|
||||
self._name = module_name
|
||||
self._target_temperature = None
|
||||
self._away = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._data.current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the current state of the thermostat."""
|
||||
state = self._data.thermostatdata.relay_cmd
|
||||
if state == 0:
|
||||
return STATE_IDLE
|
||||
elif state == 100:
|
||||
return STATE_HEAT
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
mode = "away"
|
||||
temp = None
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
mode = "program"
|
||||
temp = None
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
|
||||
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
|
||||
"""Set new target temperature for 2 hours."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
mode = "manual"
|
||||
self._data.thermostatdata.setthermpoint(
|
||||
mode, temperature, endTimeOffset)
|
||||
self._target_temperature = temperature
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from NetAtmo API and updates the states."""
|
||||
self._data.update()
|
||||
self._target_temperature = self._data.thermostatdata.setpoint_temp
|
||||
self._away = self._data.setpoint_mode == 'away'
|
||||
|
||||
|
||||
class ThermostatData(object):
|
||||
"""Get the latest data from Netatmo."""
|
||||
|
||||
def __init__(self, auth, device=None):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.thermostatdata = None
|
||||
self.module_names = []
|
||||
self.device = device
|
||||
self.current_temperature = None
|
||||
self.target_temperature = None
|
||||
self.setpoint_mode = None
|
||||
# self.operation =
|
||||
|
||||
def get_module_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.update()
|
||||
if not self.device:
|
||||
for device in self.thermostatdata.modules:
|
||||
for module in self.thermostatdata.modules[device].values():
|
||||
self.module_names.append(module['module_name'])
|
||||
else:
|
||||
for module in self.thermostatdata.modules[self.device].values():
|
||||
self.module_names.append(module['module_name'])
|
||||
return self.module_names
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.thermostatdata = lnetatmo.ThermostatData(self.auth)
|
||||
self.target_temperature = self.thermostatdata.setpoint_temp
|
||||
self.setpoint_mode = self.thermostatdata.setpoint_mode
|
||||
self.current_temperature = self.thermostatdata.temp
|
||||
@@ -12,7 +12,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['proliphix==0.3.1']
|
||||
REQUIREMENTS = ['proliphix==0.4.0']
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
|
||||
@@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([ProliphixThermostat(pdp)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class ProliphixThermostat(ClimateDevice):
|
||||
"""Representation a Proliphix thermostat."""
|
||||
|
||||
@@ -69,7 +68,7 @@ class ProliphixThermostat(ClimateDevice):
|
||||
}
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ https://home-assistant.io/components/climate.radiotherm/
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
from urllib.error import URLError
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -52,14 +51,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
try:
|
||||
tstat = radiotherm.get_thermostat(host)
|
||||
tstats.append(RadioThermostat(tstat, hold_temp))
|
||||
except (URLError, OSError):
|
||||
except OSError:
|
||||
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
|
||||
host)
|
||||
|
||||
add_devices(tstats)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class RadioThermostat(ClimateDevice):
|
||||
"""Representation of a Radio Thermostat."""
|
||||
|
||||
@@ -71,6 +69,8 @@ class RadioThermostat(ClimateDevice):
|
||||
self._current_temperature = None
|
||||
self._current_operation = STATE_IDLE
|
||||
self._name = None
|
||||
self._fmode = None
|
||||
self._tmode = None
|
||||
self.hold_temp = hold_temp
|
||||
self.update()
|
||||
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
|
||||
@@ -81,7 +81,7 @@ class RadioThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@@ -89,8 +89,8 @@ class RadioThermostat(ClimateDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
ATTR_FAN: self.device.fmode['human'],
|
||||
ATTR_MODE: self.device.tmode['human']
|
||||
ATTR_FAN: self._fmode,
|
||||
ATTR_MODE: self._tmode,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -117,10 +117,13 @@ class RadioThermostat(ClimateDevice):
|
||||
"""Update the data from the thermostat."""
|
||||
self._current_temperature = self.device.temp['raw']
|
||||
self._name = self.device.name['raw']
|
||||
if self.device.tmode['human'] == 'Cool':
|
||||
self._fmode = self.device.fmode['human']
|
||||
self._tmode = self.device.tmode['human']
|
||||
|
||||
if self._tmode == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
self._current_operation = STATE_COOL
|
||||
elif self.device.tmode['human'] == 'Heat':
|
||||
elif self._tmode == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
self._current_operation = STATE_HEAT
|
||||
else:
|
||||
@@ -132,9 +135,9 @@ class RadioThermostat(ClimateDevice):
|
||||
if temperature is None:
|
||||
return
|
||||
if self._current_operation == STATE_COOL:
|
||||
self.device.t_cool = temperature
|
||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||
elif self._current_operation == STATE_HEAT:
|
||||
self.device.t_heat = temperature
|
||||
self.device.t_heat = round(temperature * 2.0) / 2.0
|
||||
if self.hold_temp:
|
||||
self.device.hold = 1
|
||||
else:
|
||||
@@ -156,6 +159,6 @@ class RadioThermostat(ClimateDevice):
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.device.tmode = 3
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.device.t_cool = self._target_temperature
|
||||
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0
|
||||
elif operation_mode == STATE_HEAT:
|
||||
self.device.t_heat = self._target_temperature
|
||||
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0
|
||||
|
||||
@@ -8,7 +8,10 @@ import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE)
|
||||
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
@@ -28,7 +31,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
device in VERA_DEVICES['climate'])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
"""Representation of a Vera Thermostat."""
|
||||
|
||||
@@ -93,9 +95,15 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
self._state = self.vera_device.get_hvac_mode()
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
vera_temp_units = (
|
||||
self.vera_device.vera_controller.temperature_units)
|
||||
|
||||
if vera_temp_units == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
@@ -69,11 +69,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Represents a ZWave Climate device."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, value, temp_unit):
|
||||
"""Initialize the zwave climate device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
@@ -84,6 +82,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._current_temperature = None
|
||||
self._current_operation = None
|
||||
self._operation_list = None
|
||||
self._operating_state = None
|
||||
self._current_fan_mode = None
|
||||
self._fan_list = None
|
||||
self._current_swing_mode = None
|
||||
@@ -182,6 +181,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("Device can't set setpoint based on operation mode."
|
||||
" Defaulting to index=1")
|
||||
self._target_temperature = int(value.data)
|
||||
# Operating state
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE)
|
||||
.values()):
|
||||
self._operating_state = value.data
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -209,7 +213,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
return self._swing_list
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self._unit == 'C':
|
||||
return TEMP_CELSIUS
|
||||
@@ -323,3 +327,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
value.index == 33:
|
||||
value.data = bytes(swing_mode, 'utf-8')
|
||||
break
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
if self._operating_state:
|
||||
return {
|
||||
"operating_state": self._operating_state,
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
|
||||
@@ -8,7 +8,8 @@ the user has submitted configuration information.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
_INSTANCES = {}
|
||||
@@ -33,10 +34,10 @@ STATE_CONFIGURE = 'configure'
|
||||
STATE_CONFIGURED = 'configured'
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def request_config(
|
||||
hass, name, callback, description=None, description_image=None,
|
||||
submit_caption=None, fields=None, link_name=None, link_url=None):
|
||||
submit_caption=None, fields=None, link_name=None, link_url=None,
|
||||
entity_picture=None):
|
||||
"""Create a new request for configuration.
|
||||
|
||||
Will return an ID to be used for sequent calls.
|
||||
@@ -46,7 +47,7 @@ def request_config(
|
||||
request_id = instance.request_config(
|
||||
name, callback,
|
||||
description, description_image, submit_caption,
|
||||
fields, link_name, link_url)
|
||||
fields, link_name, link_url, entity_picture)
|
||||
|
||||
_REQUESTS[request_id] = instance
|
||||
|
||||
@@ -100,11 +101,10 @@ class Configurator(object):
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def request_config(
|
||||
self, name, callback,
|
||||
description, description_image, submit_caption,
|
||||
fields, link_name, link_url):
|
||||
fields, link_name, link_url, entity_picture):
|
||||
"""Setup a request for configuration."""
|
||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
|
||||
|
||||
@@ -119,6 +119,7 @@ class Configurator(object):
|
||||
ATTR_CONFIGURE_ID: request_id,
|
||||
ATTR_FIELDS: fields,
|
||||
ATTR_FRIENDLY_NAME: name,
|
||||
ATTR_ENTITY_PICTURE: entity_picture,
|
||||
}
|
||||
|
||||
data.update({
|
||||
|
||||
@@ -60,11 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class CommandCover(CoverDevice):
|
||||
"""Representation a command line cover."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, command_open, command_close, command_stop,
|
||||
command_state, value_template):
|
||||
"""Initialize the cover."""
|
||||
|
||||
@@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class DemoCover(CoverDevice):
|
||||
"""Representation of a demo cover."""
|
||||
|
||||
# pylint: disable=no-self-use, too-many-instance-attributes
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, hass, name, position=None, tilt_position=None):
|
||||
"""Initialize the cover."""
|
||||
self.hass = hass
|
||||
|
||||
275
homeassistant/components/cover/garadget.py
Normal file
275
homeassistant/components/cover/garadget.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Platform for the garadget cover component.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/garadget/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD,\
|
||||
CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN,\
|
||||
CONF_COVERS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEFAULT_NAME = 'Garadget'
|
||||
|
||||
ATTR_SIGNAL_STRENGTH = "wifi signal strength (dB)"
|
||||
ATTR_TIME_IN_STATE = "time in state"
|
||||
ATTR_SENSOR_STRENGTH = "sensor reflection rate"
|
||||
ATTR_AVAILABLE = "available"
|
||||
|
||||
STATE_OPENING = "opening"
|
||||
STATE_CLOSING = "closing"
|
||||
STATE_STOPPED = "stopped"
|
||||
STATE_OFFLINE = "offline"
|
||||
|
||||
STATES_MAP = {
|
||||
"open": STATE_OPEN,
|
||||
"opening": STATE_OPENING,
|
||||
"closed": STATE_CLOSED,
|
||||
"closing": STATE_CLOSING,
|
||||
"stopped": STATE_STOPPED
|
||||
}
|
||||
|
||||
|
||||
# Validation of the user's configuration
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_DEVICE): cv.string,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_ACCESS_TOKEN): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo covers."""
|
||||
covers = []
|
||||
devices = config.get(CONF_COVERS, {})
|
||||
|
||||
_LOGGER.debug(devices)
|
||||
|
||||
for device_id, device_config in devices.items():
|
||||
args = {
|
||||
"name": device_config.get(CONF_NAME),
|
||||
"device_id": device_config.get(CONF_DEVICE, device_id),
|
||||
"username": device_config.get(CONF_USERNAME),
|
||||
"password": device_config.get(CONF_PASSWORD),
|
||||
"access_token": device_config.get(CONF_ACCESS_TOKEN)
|
||||
}
|
||||
|
||||
covers.append(GaradgetCover(hass, args))
|
||||
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
class GaradgetCover(CoverDevice):
|
||||
"""Representation of a demo cover."""
|
||||
|
||||
# pylint: disable=no-self-use, too-many-instance-attributes
|
||||
def __init__(self, hass, args):
|
||||
"""Initialize the cover."""
|
||||
self.particle_url = 'https://api.particle.io'
|
||||
self.hass = hass
|
||||
self._name = args['name']
|
||||
self.device_id = args['device_id']
|
||||
self.access_token = args['access_token']
|
||||
self.obtained_token = False
|
||||
self._username = args['username']
|
||||
self._password = args['password']
|
||||
self._state = STATE_UNKNOWN
|
||||
self.time_in_state = None
|
||||
self.signal = None
|
||||
self.sensor = None
|
||||
self._unsub_listener_cover = None
|
||||
self._available = True
|
||||
|
||||
if self.access_token is None:
|
||||
self.access_token = self.get_token()
|
||||
self._obtained_token = True
|
||||
|
||||
# Lets try to get the configured name if not provided.
|
||||
try:
|
||||
if self._name is None:
|
||||
doorconfig = self._get_variable("doorConfig")
|
||||
if doorconfig["nme"] is not None:
|
||||
self._name = doorconfig["nme"]
|
||||
self.update()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to server: %(reason)s',
|
||||
dict(reason=ex))
|
||||
self._state = STATE_OFFLINE
|
||||
self._available = False
|
||||
self._name = DEFAULT_NAME
|
||||
except KeyError as ex:
|
||||
_LOGGER.warning('Garadget device %(device)s seems to be offline',
|
||||
dict(device=self.device_id))
|
||||
self._name = DEFAULT_NAME
|
||||
self._state = STATE_OFFLINE
|
||||
self._available = False
|
||||
|
||||
def __del__(self):
|
||||
"""Try to remove token."""
|
||||
if self._obtained_token is True:
|
||||
if self.access_token is not None:
|
||||
self.remove_token()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for a demo cover."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
data = {}
|
||||
|
||||
if self.signal is not None:
|
||||
data[ATTR_SIGNAL_STRENGTH] = self.signal
|
||||
|
||||
if self.time_in_state is not None:
|
||||
data[ATTR_TIME_IN_STATE] = self.time_in_state
|
||||
|
||||
if self.sensor is not None:
|
||||
data[ATTR_SENSOR_STRENGTH] = self.sensor
|
||||
|
||||
if self.access_token is not None:
|
||||
data[CONF_ACCESS_TOKEN] = self.access_token
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
def get_token(self):
|
||||
"""Get new token for usage during this session."""
|
||||
args = {
|
||||
'grant_type': 'password',
|
||||
'username': self._username,
|
||||
'password': self._password
|
||||
}
|
||||
url = '{}/oauth/token'.format(self.particle_url)
|
||||
ret = requests.post(url,
|
||||
auth=('particle', 'particle'),
|
||||
data=args)
|
||||
|
||||
return ret.json()['access_token']
|
||||
|
||||
def remove_token(self):
|
||||
"""Remove authorization token from API."""
|
||||
ret = requests.delete('{}/v1/access_tokens/{}'.format(
|
||||
self.particle_url,
|
||||
self.access_token),
|
||||
auth=(self._username, self._password))
|
||||
return ret.text
|
||||
|
||||
def _start_watcher(self, command):
|
||||
"""Start watcher."""
|
||||
_LOGGER.debug("Starting Watcher for command: %s ", command)
|
||||
if self._unsub_listener_cover is None:
|
||||
self._unsub_listener_cover = track_utc_time_change(
|
||||
self.hass, self._check_state)
|
||||
|
||||
def _check_state(self, now):
|
||||
"""Check the state of the service during an operation."""
|
||||
self.update()
|
||||
self.update_ha_state()
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the cover."""
|
||||
if self._state not in ["close", "closing"]:
|
||||
ret = self._put_command("setState", "close")
|
||||
self._start_watcher('close')
|
||||
return ret.get('return_value') == 1
|
||||
|
||||
def open_cover(self):
|
||||
"""Open the cover."""
|
||||
if self._state not in ["open", "opening"]:
|
||||
ret = self._put_command("setState", "open")
|
||||
self._start_watcher('open')
|
||||
return ret.get('return_value') == 1
|
||||
|
||||
def stop_cover(self):
|
||||
"""Stop the door where it is."""
|
||||
if self._state not in ["stopped"]:
|
||||
ret = self._put_command("setState", "stop")
|
||||
self._start_watcher('stop')
|
||||
return ret['return_value'] == 1
|
||||
|
||||
def update(self):
|
||||
"""Get updated status from API."""
|
||||
try:
|
||||
status = self._get_variable("doorStatus")
|
||||
_LOGGER.debug("Current Status: %s", status['status'])
|
||||
self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN)
|
||||
self.time_in_state = status['time']
|
||||
self.signal = status['signal']
|
||||
self.sensor = status['sensor']
|
||||
self._availble = True
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to server: %(reason)s',
|
||||
dict(reason=ex))
|
||||
self._state = STATE_OFFLINE
|
||||
except KeyError as ex:
|
||||
_LOGGER.warning('Garadget device %(device)s seems to be offline',
|
||||
dict(device=self.device_id))
|
||||
self._state = STATE_OFFLINE
|
||||
|
||||
if self._state not in [STATE_CLOSING, STATE_OPENING]:
|
||||
if self._unsub_listener_cover is not None:
|
||||
self._unsub_listener_cover()
|
||||
self._unsub_listener_cover = None
|
||||
|
||||
def _get_variable(self, var):
|
||||
"""Get latest status."""
|
||||
url = '{}/v1/devices/{}/{}?access_token={}'.format(
|
||||
self.particle_url,
|
||||
self.device_id,
|
||||
var,
|
||||
self.access_token,
|
||||
)
|
||||
ret = requests.get(url)
|
||||
result = {}
|
||||
for pairs in ret.json()['result'].split('|'):
|
||||
key = pairs.split('=')
|
||||
result[key[0]] = key[1]
|
||||
return result
|
||||
|
||||
def _put_command(self, func, arg=None):
|
||||
"""Send commands to API."""
|
||||
params = {'access_token': self.access_token}
|
||||
if arg:
|
||||
params['command'] = arg
|
||||
url = '{}/v1/devices/{}/{}'.format(
|
||||
self.particle_url,
|
||||
self.device_id,
|
||||
func)
|
||||
ret = requests.post(url, data=params)
|
||||
return ret.json()
|
||||
@@ -31,7 +31,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HMCover(homematic.HMDevice, CoverDevice):
|
||||
"""Represents a Homematic Cover in Home Assistant."""
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttCover(CoverDevice):
|
||||
"""Representation of a cover that can be controlled using MQTT."""
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice):
|
||||
"""Representation of an rfxtrx cover."""
|
||||
|
||||
|
||||
@@ -63,11 +63,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class RPiGPIOCover(CoverDevice):
|
||||
"""Representation of a Raspberry GPIO cover."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, relay_pin, state_pin, state_pull_mode,
|
||||
relay_time):
|
||||
"""Initialize the cover."""
|
||||
|
||||
@@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class SCSGateCover(CoverDevice):
|
||||
"""Representation of SCSGate cover."""
|
||||
|
||||
|
||||
@@ -15,14 +15,13 @@ DEPENDENCIES = ['vera']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Vera covers."""
|
||||
add_devices_callback(
|
||||
add_devices(
|
||||
VeraCover(device, VERA_CONTROLLER) for
|
||||
device in VERA_DEVICES['cover'])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VeraCover(VeraDevice, CoverDevice):
|
||||
"""Represents a Vera Cover in Home Assistant."""
|
||||
|
||||
|
||||
@@ -32,17 +32,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \
|
||||
and value.index == 0:
|
||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and value.index == 0):
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \
|
||||
node.has_command_class(zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if value.type != zwave.const.TYPE_BOOL and \
|
||||
value.genre != zwave.const.GENRE_USER:
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL:
|
||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class ==
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if (value.type != zwave.const.TYPE_BOOL and
|
||||
value.genre != zwave.const.GENRE_USER):
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
else:
|
||||
return
|
||||
|
||||
@@ -122,7 +124,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Open' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Down':
|
||||
'Up':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
|
||||
@@ -132,7 +134,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Up' or value.command_class == \
|
||||
'Down' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Close':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
|
||||
@@ -67,31 +67,33 @@ def setup(hass, config):
|
||||
lights = sorted(hass.states.entity_ids('light'))
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
media_players = sorted(hass.states.entity_ids('media_player'))
|
||||
group.Group(hass, 'living room', [
|
||||
|
||||
group.Group.create_group(hass, 'living room', [
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'scene.romantic_lights'])
|
||||
group.Group(hass, 'bedroom', [
|
||||
group.Group.create_group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance'])
|
||||
group.Group(hass, 'kitchen', [
|
||||
group.Group.create_group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
|
||||
group.Group(hass, 'doors', [
|
||||
group.Group.create_group(hass, 'doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door'])
|
||||
group.Group(hass, 'automations', [
|
||||
group.Group.create_group(hass, 'automations', [
|
||||
'input_select.who_cooks', 'input_boolean.notify', ])
|
||||
group.Group(hass, 'people', [
|
||||
group.Group.create_group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus'])
|
||||
group.Group(hass, 'thermostats', [
|
||||
group.Group.create_group(hass, 'thermostats', [
|
||||
'thermostat.nest', 'thermostat.thermostat'])
|
||||
group.Group(hass, 'downstairs', [
|
||||
group.Group.create_group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window', 'group.doors', 'thermostat.nest',
|
||||
'rollershutter.living_room_window', 'group.doors',
|
||||
'thermostat.nest',
|
||||
], view=True)
|
||||
group.Group(hass, 'Upstairs', [
|
||||
group.Group.create_group(hass, 'Upstairs', [
|
||||
'thermostat.thermostat', 'group.bedroom',
|
||||
], view=True)
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def setup(hass, config):
|
||||
"""The triggers to turn lights on or off based on device presence."""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -4,8 +4,7 @@ Provide functionality to keep track of devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker/
|
||||
"""
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
# pylint: disable=too-many-locals
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
@@ -25,12 +24,14 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.yaml import dump
|
||||
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID)
|
||||
|
||||
DOMAIN = 'device_tracker'
|
||||
DEPENDENCIES = ['zone']
|
||||
@@ -54,6 +55,8 @@ DEFAULT_SCAN_INTERVAL = 12
|
||||
CONF_AWAY_HIDE = 'hide_if_away'
|
||||
DEFAULT_AWAY_HIDE = False
|
||||
|
||||
EVENT_NEW_DEVICE = 'device_tracker_new_device'
|
||||
|
||||
SERVICE_SEE = 'see'
|
||||
|
||||
ATTR_MAC = 'mac'
|
||||
@@ -66,15 +69,11 @@ ATTR_ATTRIBUTES = 'attributes'
|
||||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
|
||||
vol.Schema({
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_CONSIDER_HOME, default=timedelta(seconds=180)): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
}, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA)
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_CONSIDER_HOME,
|
||||
default=timedelta(seconds=DEFAULT_CONSIDER_HOME)): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
SERVICE_NETGEAR: 'netgear',
|
||||
@@ -89,7 +88,6 @@ def is_on(hass: HomeAssistantType, entity_id: str=None):
|
||||
return hass.states.is_state(entity, STATE_HOME)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
|
||||
host_name: str=None, location_name: str=None,
|
||||
gps: GPSType=None, gps_accuracy=None,
|
||||
@@ -104,8 +102,7 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
|
||||
(ATTR_GPS_ACCURACY, gps_accuracy),
|
||||
(ATTR_BATTERY, battery)) if value is not None}
|
||||
if attributes:
|
||||
for key, value in attributes:
|
||||
data[key] = value
|
||||
data[ATTR_ATTRIBUTES] = attributes
|
||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||
|
||||
|
||||
@@ -114,9 +111,9 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
|
||||
try:
|
||||
conf = _CONFIG_SCHEMA(config).get(DOMAIN, [])
|
||||
conf = config.get(DOMAIN, [])
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, DOMAIN, config)
|
||||
log_exception(ex, DOMAIN, config, hass)
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
@@ -241,9 +238,15 @@ class DeviceTracker(object):
|
||||
|
||||
device.seen(host_name, location_name, gps, gps_accuracy, battery,
|
||||
attributes)
|
||||
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
|
||||
self.hass.bus.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 is not None:
|
||||
self.group.update_tracked_entity_ids(
|
||||
@@ -252,9 +255,18 @@ class DeviceTracker(object):
|
||||
|
||||
def setup_group(self):
|
||||
"""Initialize group for all tracked devices."""
|
||||
run_coroutine_threadsafe(
|
||||
self.async_setup_group(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_group(self):
|
||||
"""Initialize group for all tracked devices.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||
if dev.track)
|
||||
self.group = group.Group(
|
||||
self.group = yield from group.Group.async_create_group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
|
||||
def update_stale(self, now: dt_util.dt.datetime):
|
||||
@@ -413,13 +425,18 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
})
|
||||
try:
|
||||
result = []
|
||||
devices = load_yaml_config_file(path)
|
||||
try:
|
||||
devices = load_yaml_config_file(path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||
return []
|
||||
|
||||
for dev_id, device in devices.items():
|
||||
try:
|
||||
device = dev_schema(device)
|
||||
device['dev_id'] = cv.slugify(dev_id)
|
||||
except vol.Invalid as exp:
|
||||
log_exception(exp, dev_id, devices)
|
||||
log_exception(exp, dev_id, devices, hass)
|
||||
else:
|
||||
result.append(Device(hass, **device))
|
||||
return result
|
||||
@@ -455,15 +472,15 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
def update_config(path: str, dev_id: str, device: Device):
|
||||
"""Add device to YAML configuration file."""
|
||||
with open(path, 'a') as out:
|
||||
device = {device.dev_id: {
|
||||
'name': device.name,
|
||||
'mac': device.mac,
|
||||
'picture': device.config_picture,
|
||||
'track': device.track,
|
||||
CONF_AWAY_HIDE: device.away_hide
|
||||
}}
|
||||
out.write('\n')
|
||||
out.write('{}:\n'.format(device.dev_id))
|
||||
|
||||
for key, value in (('name', device.name), ('mac', device.mac),
|
||||
('picture', device.config_picture),
|
||||
('track', 'yes' if device.track else 'no'),
|
||||
(CONF_AWAY_HIDE,
|
||||
'yes' if device.away_hide else 'no')):
|
||||
out.write(' {}: {}\n'.format(key, '' if value is None else value))
|
||||
out.write(dump(device))
|
||||
|
||||
|
||||
def get_gravatar_for_email(email: str):
|
||||
|
||||
@@ -90,9 +90,7 @@ AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
|
||||
class AsusWrtDeviceScanner(object):
|
||||
"""This class queries a router running ASUSWRT firmware."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-branches
|
||||
# Eighth attribute needed for mode (AP mode vs router mode)
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
|
||||
@@ -60,8 +60,6 @@ def setup_scanner(hass, config: dict, see):
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-few-public-methods
|
||||
class AutomaticDeviceScanner(object):
|
||||
"""A class representing an Automatic device."""
|
||||
|
||||
|
||||
83
homeassistant/components/device_tracker/bbox.py
Normal file
83
homeassistant/components/device_tracker/bbox.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Support for French FAI Bouygues Bbox routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.bbox/
|
||||
"""
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pybbox==0.0.5-alpha']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Bbox scanner."""
|
||||
scanner = BboxDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
|
||||
|
||||
|
||||
class BboxDeviceScanner(object):
|
||||
"""This class scans for devices connected to the bbox."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.last_results = [] # type: List[Device]
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info("Bbox 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 given device or None if we don't know."""
|
||||
filter_named = [device.name for device in self.last_results if
|
||||
device.mac == mac]
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Check the bbox for devices.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info("Scanning...")
|
||||
|
||||
import pybbox
|
||||
|
||||
box = pybbox.Bbox()
|
||||
result = box.get_all_connected_devices()
|
||||
|
||||
now = dt_util.now()
|
||||
last_results = []
|
||||
for device in result:
|
||||
if device['active'] != 1:
|
||||
continue
|
||||
last_results.append(
|
||||
Device(device['macaddress'], device['hostname'],
|
||||
device['ipaddress'], now))
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info("Bbox scan successful")
|
||||
return True
|
||||
@@ -35,12 +35,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a DD-WRT scanner."""
|
||||
scanner = DdWrtDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
try:
|
||||
return DdWrtDeviceScanner(config[DOMAIN])
|
||||
except ConnectionError:
|
||||
return None
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class DdWrtDeviceScanner(object):
|
||||
"""This class queries a wireless router running DD-WRT firmware."""
|
||||
|
||||
@@ -53,13 +53,13 @@ class DdWrtDeviceScanner(object):
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.mac2name = {}
|
||||
|
||||
# Test the router is accessible
|
||||
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
self.success_init = data is not None
|
||||
if not data:
|
||||
raise ConnectionError('Cannot connect to DD-Wrt router')
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
@@ -83,14 +83,15 @@ class DdWrtDeviceScanner(object):
|
||||
if not dhcp_leases:
|
||||
return None
|
||||
|
||||
# Remove leading and trailing single quotes.
|
||||
cleaned_str = dhcp_leases.strip().strip('"')
|
||||
elements = cleaned_str.split('","')
|
||||
num_clients = int(len(elements)/5)
|
||||
# Remove leading and trailing quotes and spaces
|
||||
cleaned_str = dhcp_leases.replace(
|
||||
"\"", "").replace("\'", "").replace(" ", "")
|
||||
elements = cleaned_str.split(',')
|
||||
num_clients = int(len(elements) / 5)
|
||||
self.mac2name = {}
|
||||
for idx in range(0, num_clients):
|
||||
# This is stupid but the data is a single array
|
||||
# every 5 elements represents one hosts, the MAC
|
||||
# The data is a single array
|
||||
# every 5 elements represents one host, the MAC
|
||||
# is the third element and the name is the first.
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
@@ -105,9 +106,6 @@ class DdWrtDeviceScanner(object):
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info('Checking ARP')
|
||||
|
||||
@@ -123,11 +121,8 @@ class DdWrtDeviceScanner(object):
|
||||
if not active_clients:
|
||||
return False
|
||||
|
||||
# This is really lame, instead of using JSON the DD-WRT UI
|
||||
# uses its own data format for some reason and then
|
||||
# regex's out values so I guess I have to do the same,
|
||||
# LAME!!!
|
||||
|
||||
# The DD-WRT UI uses its own data format and then
|
||||
# regex's out values so this is done here too
|
||||
# Remove leading and trailing single quotes.
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
|
||||
@@ -38,7 +38,6 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class FritzBoxScanner(object):
|
||||
"""This class queries a FRITZ!Box router."""
|
||||
|
||||
@@ -79,7 +78,7 @@ class FritzBoxScanner(object):
|
||||
self._update_info()
|
||||
active_hosts = []
|
||||
for known_host in self.last_results:
|
||||
if known_host['status'] == '1':
|
||||
if known_host['status'] == '1' and known_host.get('mac'):
|
||||
active_hosts.append(known_host['mac'])
|
||||
return active_hosts
|
||||
|
||||
|
||||
@@ -1,100 +1,427 @@
|
||||
"""
|
||||
Support for iCloud connected devices.
|
||||
Platform that supports scanning iCloud.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.icloud/
|
||||
"""
|
||||
import logging
|
||||
import random
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.zone import active_zone
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT,
|
||||
PLATFORM_SCHEMA)
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.location import distance
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyicloud==0.9.1']
|
||||
|
||||
CONF_INTERVAL = 'interval'
|
||||
KEEPALIVE_INTERVAL = 4
|
||||
CONF_IGNORED_DEVICES = 'ignored_devices'
|
||||
CONF_ACCOUNTNAME = 'account_name'
|
||||
|
||||
# entity attributes
|
||||
ATTR_ACCOUNTNAME = 'account_name'
|
||||
ATTR_INTERVAL = 'interval'
|
||||
ATTR_DEVICENAME = 'device_name'
|
||||
ATTR_BATTERY = 'battery'
|
||||
ATTR_DISTANCE = 'distance'
|
||||
ATTR_DEVICESTATUS = 'device_status'
|
||||
ATTR_LOWPOWERMODE = 'low_power_mode'
|
||||
ATTR_BATTERYSTATUS = 'battery_status'
|
||||
|
||||
ICLOUDTRACKERS = {}
|
||||
|
||||
_CONFIGURING = {}
|
||||
|
||||
DEVICESTATUSSET = ['features', 'maxMsgChar', 'darkWake', 'fmlyShare',
|
||||
'deviceStatus', 'remoteLock', 'activationLocked',
|
||||
'deviceClass', 'id', 'deviceModel', 'rawDeviceModel',
|
||||
'passcodeLength', 'canWipeAfterLock', 'trackingInfo',
|
||||
'location', 'msg', 'batteryLevel', 'remoteWipe',
|
||||
'thisDevice', 'snd', 'prsId', 'wipeInProgress',
|
||||
'lowPowerMode', 'lostModeEnabled', 'isLocating',
|
||||
'lostModeCapable', 'mesg', 'name', 'batteryStatus',
|
||||
'lockedTimestamp', 'lostTimestamp', 'locationCapable',
|
||||
'deviceDisplayName', 'lostDevice', 'deviceColor',
|
||||
'wipedTimestamp', 'modelDisplayName', 'locationEnabled',
|
||||
'isMac', 'locFoundEnabled']
|
||||
|
||||
DEVICESTATUSCODES = {'200': 'online', '201': 'offline', '203': 'pending',
|
||||
'204': 'unregistered'}
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]),
|
||||
vol.Optional(ATTR_DEVICENAME): cv.slugify,
|
||||
vol.Optional(ATTR_INTERVAL): cv.positive_int,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): vol.Coerce(str),
|
||||
vol.Required(CONF_PASSWORD): vol.Coerce(str),
|
||||
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1))
|
||||
})
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(ATTR_ACCOUNTNAME): cv.slugify,
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup the iCloud Scanner."""
|
||||
from pyicloud import PyiCloudService
|
||||
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
|
||||
def setup_scanner(hass, config: dict, see):
|
||||
"""Set up the iCloud Scanner."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0]))
|
||||
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
icloudaccount = Icloud(hass, username, password, account, see)
|
||||
|
||||
try:
|
||||
_LOGGER.info('Logging into iCloud Account')
|
||||
# Attempt the login to iCloud
|
||||
api = PyiCloudService(username, password, verify=True)
|
||||
except PyiCloudFailedLoginException as error:
|
||||
_LOGGER.exception('Error logging into iCloud Service: %s', error)
|
||||
if icloudaccount.api is not None:
|
||||
ICLOUDTRACKERS[account] = icloudaccount
|
||||
|
||||
else:
|
||||
_LOGGER.error("No ICLOUDTRACKERS added")
|
||||
return False
|
||||
|
||||
def keep_alive(now):
|
||||
"""Keep authenticating iCloud connection.
|
||||
def lost_iphone(call):
|
||||
"""Call the lost iphone function if the device is found."""
|
||||
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
||||
devicename = call.data.get(ATTR_DEVICENAME)
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].lost_iphone(devicename)
|
||||
hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
The session timeouts if we are not using it so we
|
||||
have to re-authenticate & this will send an email.
|
||||
"""
|
||||
api.authenticate()
|
||||
_LOGGER.info("Authenticate against iCloud")
|
||||
def update_icloud(call):
|
||||
"""Call the update function of an icloud account."""
|
||||
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
||||
devicename = call.data.get(ATTR_DEVICENAME)
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].update_icloud(devicename)
|
||||
hass.services.register(DOMAIN, 'icloud_update', update_icloud,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
seen_devices = {}
|
||||
def reset_account_icloud(call):
|
||||
"""Reset an icloud account."""
|
||||
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].reset_account_icloud()
|
||||
hass.services.register(DOMAIN, 'icloud_reset_account',
|
||||
reset_account_icloud, schema=SERVICE_SCHEMA)
|
||||
|
||||
def update_icloud(now):
|
||||
"""Authenticate against iCloud and scan for devices."""
|
||||
try:
|
||||
keep_alive(None)
|
||||
# Loop through every device registered with the iCloud account
|
||||
for device in api.devices:
|
||||
status = device.status()
|
||||
dev_id = slugify(status['name'].replace(' ', '', 99))
|
||||
def setinterval(call):
|
||||
"""Call the update function of an icloud account."""
|
||||
accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
|
||||
interval = call.data.get(ATTR_INTERVAL)
|
||||
devicename = call.data.get(ATTR_DEVICENAME)
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].setinterval(interval, devicename)
|
||||
|
||||
# An entity will not be created by see() when track=false in
|
||||
# 'known_devices.yaml', but we need to see() it at least once
|
||||
entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id))
|
||||
if entity is None and dev_id in seen_devices:
|
||||
continue
|
||||
seen_devices[dev_id] = True
|
||||
|
||||
location = device.location()
|
||||
# If the device has a location add it. If not do nothing
|
||||
if location:
|
||||
see(
|
||||
dev_id=dev_id,
|
||||
host_name=status['name'],
|
||||
gps=(location['latitude'], location['longitude']),
|
||||
battery=status['batteryLevel']*100,
|
||||
gps_accuracy=location['horizontalAccuracy']
|
||||
)
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.info('No iCloud Devices found!')
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
|
||||
|
||||
update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
|
||||
# Schedule keepalives between the updates
|
||||
keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL)
|
||||
if x not in update_minutes)
|
||||
|
||||
track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes)
|
||||
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes)
|
||||
hass.services.register(DOMAIN, 'icloud_set_interval', setinterval,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
# Tells the bootstrapper that the component was successfully initialized
|
||||
return True
|
||||
|
||||
|
||||
class Icloud(object):
|
||||
"""Represent an icloud account in Home Assistant."""
|
||||
|
||||
def __init__(self, hass, username, password, name, see):
|
||||
"""Initialize an iCloud account."""
|
||||
self.hass = hass
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.api = None
|
||||
self.accountname = name
|
||||
self.devices = {}
|
||||
self.seen_devices = {}
|
||||
self._overridestates = {}
|
||||
self._intervals = {}
|
||||
self.see = see
|
||||
|
||||
self._trusted_device = None
|
||||
self._verification_code = None
|
||||
|
||||
self._attrs = {}
|
||||
self._attrs[ATTR_ACCOUNTNAME] = name
|
||||
|
||||
self.reset_account_icloud()
|
||||
|
||||
randomseconds = random.randint(10, 59)
|
||||
track_utc_time_change(
|
||||
self.hass, self.keep_alive,
|
||||
second=randomseconds
|
||||
)
|
||||
|
||||
def reset_account_icloud(self):
|
||||
"""Reset an icloud account."""
|
||||
from pyicloud import PyiCloudService
|
||||
from pyicloud.exceptions import (
|
||||
PyiCloudFailedLoginException, PyiCloudNoDevicesException)
|
||||
|
||||
icloud_dir = self.hass.config.path('icloud')
|
||||
if not os.path.exists(icloud_dir):
|
||||
os.makedirs(icloud_dir)
|
||||
|
||||
try:
|
||||
self.api = PyiCloudService(
|
||||
self.username, self.password,
|
||||
cookie_directory=icloud_dir,
|
||||
verify=True)
|
||||
except PyiCloudFailedLoginException as error:
|
||||
self.api = None
|
||||
_LOGGER.error('Error logging into iCloud Service: %s', error)
|
||||
return
|
||||
|
||||
try:
|
||||
self.devices = {}
|
||||
self._overridestates = {}
|
||||
self._intervals = {}
|
||||
for device in self.api.devices:
|
||||
status = device.status(DEVICESTATUSSET)
|
||||
devicename = slugify(status['name'].replace(' ', '', 99))
|
||||
if devicename not in self.devices:
|
||||
self.devices[devicename] = device
|
||||
self._intervals[devicename] = 1
|
||||
self._overridestates[devicename] = None
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error('No iCloud Devices found!')
|
||||
|
||||
def icloud_trusted_device_callback(self, callback_data):
|
||||
"""The trusted device is chosen."""
|
||||
self._trusted_device = int(callback_data.get('0', '0'))
|
||||
self._trusted_device = self.api.trusted_devices[self._trusted_device]
|
||||
if self.accountname in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(self.accountname)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
|
||||
def icloud_need_trusted_device(self):
|
||||
"""We need a trusted device."""
|
||||
configurator = get_component('configurator')
|
||||
if self.accountname in _CONFIGURING:
|
||||
return
|
||||
|
||||
devicesstring = ''
|
||||
devices = self.api.trusted_devices
|
||||
for i, device in enumerate(devices):
|
||||
devicesstring += "{}: {};".format(i, device.get('deviceName'))
|
||||
|
||||
_CONFIGURING[self.accountname] = configurator.request_config(
|
||||
self.hass, 'iCloud {}'.format(self.accountname),
|
||||
self.icloud_trusted_device_callback,
|
||||
description=(
|
||||
'Please choose your trusted device by entering'
|
||||
' the index from this list: ' + devicesstring),
|
||||
entity_picture="/static/images/config_icloud.png",
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': '0'}]
|
||||
)
|
||||
|
||||
def icloud_verification_callback(self, callback_data):
|
||||
"""The trusted device is chosen."""
|
||||
self._verification_code = callback_data.get('0')
|
||||
if self.accountname in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(self.accountname)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
|
||||
def icloud_need_verification_code(self):
|
||||
"""We need a verification code."""
|
||||
configurator = get_component('configurator')
|
||||
if self.accountname in _CONFIGURING:
|
||||
return
|
||||
|
||||
if self.api.send_verification_code(self._trusted_device):
|
||||
self._verification_code = 'waiting'
|
||||
|
||||
_CONFIGURING[self.accountname] = configurator.request_config(
|
||||
self.hass, 'iCloud {}'.format(self.accountname),
|
||||
self.icloud_verification_callback,
|
||||
description=('Please enter the validation code:'),
|
||||
entity_picture="/static/images/config_icloud.png",
|
||||
submit_caption='Confirm',
|
||||
fields=[{'code': '0'}]
|
||||
)
|
||||
|
||||
def keep_alive(self, now):
|
||||
"""Keep the api alive."""
|
||||
from pyicloud.exceptions import PyiCloud2FARequiredError
|
||||
|
||||
if self.api is None:
|
||||
self.reset_account_icloud()
|
||||
|
||||
if self.api is None:
|
||||
return
|
||||
|
||||
if self.api.requires_2fa:
|
||||
try:
|
||||
self.api.authenticate()
|
||||
except PyiCloud2FARequiredError:
|
||||
if self._trusted_device is None:
|
||||
self.icloud_need_trusted_device()
|
||||
return
|
||||
|
||||
if self._verification_code is None:
|
||||
self.icloud_need_verification_code()
|
||||
return
|
||||
|
||||
if self._verification_code == 'waiting':
|
||||
return
|
||||
|
||||
if self.api.validate_verification_code(
|
||||
self._trusted_device, self._verification_code):
|
||||
self._verification_code = None
|
||||
else:
|
||||
self.api.authenticate()
|
||||
|
||||
currentminutes = dt_util.now().hour * 60 + dt_util.now().minute
|
||||
for devicename in self.devices:
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
if ((currentminutes % interval == 0) or
|
||||
(interval > 10 and
|
||||
currentminutes % interval in [2, 4])):
|
||||
self.update_device(devicename)
|
||||
|
||||
def determine_interval(self, devicename, latitude, longitude, battery):
|
||||
"""Calculate new interval."""
|
||||
distancefromhome = None
|
||||
zone_state = self.hass.states.get('zone.home')
|
||||
zone_state_lat = zone_state.attributes['latitude']
|
||||
zone_state_long = zone_state.attributes['longitude']
|
||||
distancefromhome = distance(latitude, longitude, zone_state_lat,
|
||||
zone_state_long)
|
||||
distancefromhome = round(distancefromhome / 1000, 1)
|
||||
|
||||
currentzone = active_zone(self.hass, latitude, longitude)
|
||||
|
||||
if ((currentzone is not None and
|
||||
currentzone == self._overridestates.get(devicename)) or
|
||||
(currentzone is None and
|
||||
self._overridestates.get(devicename) == 'away')):
|
||||
return
|
||||
|
||||
self._overridestates[devicename] = None
|
||||
|
||||
if currentzone is not None:
|
||||
self._intervals[devicename] = 30
|
||||
return
|
||||
|
||||
if distancefromhome is None:
|
||||
return
|
||||
if distancefromhome > 25:
|
||||
self._intervals[devicename] = round(distancefromhome / 2, 0)
|
||||
elif distancefromhome > 10:
|
||||
self._intervals[devicename] = 5
|
||||
else:
|
||||
self._intervals[devicename] = 1
|
||||
if battery is not None and battery <= 33 and distancefromhome > 3:
|
||||
self._intervals[devicename] = self._intervals[devicename] * 2
|
||||
|
||||
def update_device(self, devicename):
|
||||
"""Update the device_tracker entity."""
|
||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
|
||||
# An entity will not be created by see() when track=false in
|
||||
# 'known_devices.yaml', but we need to see() it at least once
|
||||
entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename))
|
||||
if entity is None and devicename in self.seen_devices:
|
||||
return
|
||||
attrs = {}
|
||||
kwargs = {}
|
||||
|
||||
if self.api is None:
|
||||
return
|
||||
|
||||
try:
|
||||
for device in self.api.devices:
|
||||
if str(device) != str(self.devices[devicename]):
|
||||
continue
|
||||
|
||||
status = device.status(DEVICESTATUSSET)
|
||||
dev_id = status['name'].replace(' ', '', 99)
|
||||
dev_id = slugify(dev_id)
|
||||
attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get(
|
||||
status['deviceStatus'], 'error')
|
||||
attrs[ATTR_LOWPOWERMODE] = status['lowPowerMode']
|
||||
attrs[ATTR_BATTERYSTATUS] = status['batteryStatus']
|
||||
attrs[ATTR_ACCOUNTNAME] = self.accountname
|
||||
status = device.status(DEVICESTATUSSET)
|
||||
battery = status.get('batteryLevel', 0) * 100
|
||||
location = status['location']
|
||||
if location:
|
||||
self.determine_interval(
|
||||
devicename, location['latitude'],
|
||||
location['longitude'], battery)
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
attrs[ATTR_INTERVAL] = interval
|
||||
accuracy = location['horizontalAccuracy']
|
||||
kwargs['dev_id'] = dev_id
|
||||
kwargs['host_name'] = status['name']
|
||||
kwargs['gps'] = (location['latitude'],
|
||||
location['longitude'])
|
||||
kwargs['battery'] = battery
|
||||
kwargs['gps_accuracy'] = accuracy
|
||||
kwargs[ATTR_ATTRIBUTES] = attrs
|
||||
self.see(**kwargs)
|
||||
self.seen_devices[devicename] = True
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error('No iCloud Devices found!')
|
||||
|
||||
def lost_iphone(self, devicename):
|
||||
"""Call the lost iphone function if the device is found."""
|
||||
if self.api is None:
|
||||
return
|
||||
|
||||
self.api.authenticate()
|
||||
|
||||
for device in self.api.devices:
|
||||
if devicename is None or device == self.devices[devicename]:
|
||||
device.play_sound()
|
||||
|
||||
def update_icloud(self, devicename=None):
|
||||
"""Authenticate against iCloud and scan for devices."""
|
||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
|
||||
if self.api is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if devicename is not None:
|
||||
if devicename in self.devices:
|
||||
self.devices[devicename].update_icloud()
|
||||
else:
|
||||
_LOGGER.error("devicename %s unknown for account %s",
|
||||
devicename, self._attrs[ATTR_ACCOUNTNAME])
|
||||
else:
|
||||
for device in self.devices:
|
||||
self.devices[device].update_icloud()
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error('No iCloud Devices found!')
|
||||
|
||||
def setinterval(self, interval=None, devicename=None):
|
||||
"""Set the interval of the given devices."""
|
||||
devs = [devicename] if devicename else self.devices
|
||||
for device in devs:
|
||||
devid = DOMAIN + '.' + device
|
||||
devicestate = self.hass.states.get(devid)
|
||||
if interval is not None:
|
||||
if devicestate is not None:
|
||||
self._overridestates[device] = active_zone(
|
||||
self.hass,
|
||||
float(devicestate.attributes.get('latitude', 0)),
|
||||
float(devicestate.attributes.get('longitude', 0)))
|
||||
if self._overridestates[device] is None:
|
||||
self._overridestates[device] = 'away'
|
||||
self._intervals[device] = interval
|
||||
else:
|
||||
self._overridestates[device] = None
|
||||
self.update_device(device)
|
||||
|
||||
@@ -4,6 +4,8 @@ Support for the Locative platform.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.locative/
|
||||
"""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
||||
@@ -19,7 +21,7 @@ DEPENDENCIES = ['http']
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup an endpoint for the Locative application."""
|
||||
hass.wsgi.register_view(LocativeView(hass, see))
|
||||
hass.http.register_view(LocativeView(hass, see))
|
||||
|
||||
return True
|
||||
|
||||
@@ -35,15 +37,23 @@ class LocativeView(HomeAssistantView):
|
||||
super().__init__(hass)
|
||||
self.see = see
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Locative message received as GET."""
|
||||
return self.post(request)
|
||||
res = yield from self._handle(request.GET)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Locative message received."""
|
||||
# pylint: disable=too-many-return-statements
|
||||
data = request.values
|
||||
data = yield from request.post()
|
||||
res = yield from self._handle(data)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=too-many-return-statements
|
||||
def _handle(self, data):
|
||||
"""Handle locative request."""
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
return ('Latitude and longitude not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
@@ -68,7 +78,9 @@ class LocativeView(HomeAssistantView):
|
||||
direction = data['trigger']
|
||||
|
||||
if direction == 'enter':
|
||||
self.see(dev_id=device, location_name=location_name)
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
location_name=location_name))
|
||||
return 'Setting location to {}'.format(location_name)
|
||||
|
||||
elif direction == 'exit':
|
||||
@@ -76,7 +88,9 @@ class LocativeView(HomeAssistantView):
|
||||
'{}.{}'.format(DOMAIN, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
self.see(dev_id=device, location_name=STATE_NOT_HOME)
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
location_name=STATE_NOT_HOME))
|
||||
return 'Setting location to not home'
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
|
||||
@@ -37,7 +37,6 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class LuciDeviceScanner(object):
|
||||
"""This class queries a wireless router running OpenWrt firmware.
|
||||
|
||||
|
||||
@@ -11,13 +11,14 @@ import voluptuous as vol
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import CONF_DEVICES
|
||||
from homeassistant.components.mqtt import CONF_QOS
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
|
||||
vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic},
|
||||
})
|
||||
|
||||
|
||||
@@ -18,19 +18,19 @@ from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_HOSTS
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
REQUIREMENTS = ['python-nmap==0.6.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_EXCLUDE = 'exclude'
|
||||
# Interval in minutes to exclude devices from a scan while they are home
|
||||
CONF_HOME_INTERVAL = 'home_interval'
|
||||
CONF_EXCLUDE = 'exclude'
|
||||
|
||||
REQUIREMENTS = ['python-nmap==0.6.1']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOSTS): cv.string,
|
||||
vol.Required(CONF_HOSTS): cv.ensure_list,
|
||||
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_EXCLUDE, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Length(min=1))
|
||||
@@ -73,7 +73,7 @@ class NmapDeviceScanner(object):
|
||||
self.home_interval = timedelta(minutes=minutes)
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info('nmap scanner initialized')
|
||||
_LOGGER.info("nmap scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
@@ -97,7 +97,7 @@ class NmapDeviceScanner(object):
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info('Scanning')
|
||||
_LOGGER.info("Scanning...")
|
||||
|
||||
from nmap import PortScanner, PortScannerError
|
||||
scanner = PortScanner()
|
||||
@@ -120,7 +120,8 @@ class NmapDeviceScanner(object):
|
||||
options += ' --exclude {}'.format(','.join(exclude_hosts))
|
||||
|
||||
try:
|
||||
result = scanner.scan(hosts=self.hosts, arguments=options)
|
||||
result = scanner.scan(hosts=' '.join(self.hosts),
|
||||
arguments=options)
|
||||
except PortScannerError:
|
||||
return False
|
||||
|
||||
@@ -137,5 +138,5 @@ class NmapDeviceScanner(object):
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info('nmap scan successful')
|
||||
_LOGGER.info("nmap scan successful")
|
||||
return True
|
||||
|
||||
@@ -114,10 +114,9 @@ def setup_scanner(hass, config, see):
|
||||
'for topic %s.', topic)
|
||||
return None
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def validate_payload(topic, payload, data_type):
|
||||
"""Validate the OwnTracks payload."""
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
except ValueError:
|
||||
@@ -143,9 +142,9 @@ def setup_scanner(hass, config, see):
|
||||
return data
|
||||
if max_gps_accuracy is not None and \
|
||||
convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
|
||||
_LOGGER.warning('Ignoring %s update because expected GPS '
|
||||
'accuracy %s is not met: %s',
|
||||
data_type, max_gps_accuracy, payload)
|
||||
_LOGGER.info('Ignoring %s update because expected GPS '
|
||||
'accuracy %s is not met: %s',
|
||||
data_type, max_gps_accuracy, payload)
|
||||
return None
|
||||
if convert(data.get('acc'), float, 1.0) == 0.0:
|
||||
_LOGGER.warning('Ignoring %s update because GPS accuracy'
|
||||
@@ -248,7 +247,7 @@ def setup_scanner(hass, config, see):
|
||||
if (max_gps_accuracy is not None and
|
||||
data['acc'] > max_gps_accuracy):
|
||||
valid_gps = False
|
||||
_LOGGER.warning(
|
||||
_LOGGER.info(
|
||||
'Ignoring GPS in region exit because expected '
|
||||
'GPS accuracy %s is not met: %s',
|
||||
max_gps_accuracy, payload)
|
||||
|
||||
@@ -31,3 +31,48 @@ see:
|
||||
battery:
|
||||
description: Battery level of device
|
||||
example: '100'
|
||||
|
||||
icloud:
|
||||
icloud_lost_iphone:
|
||||
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.
|
||||
example: 'bart'
|
||||
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
|
||||
|
||||
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.
|
||||
example: 'bart'
|
||||
device_name:
|
||||
description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account.
|
||||
example: 'iphonebart'
|
||||
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.
|
||||
example: 'bart'
|
||||
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.
|
||||
example: 'bart'
|
||||
|
||||
@@ -23,11 +23,17 @@ _LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pysnmp==4.3.2']
|
||||
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_AUTHKEY = "authkey"
|
||||
CONF_PRIVKEY = "privkey"
|
||||
CONF_BASEOID = "baseoid"
|
||||
|
||||
DEFAULT_COMMUNITY = "public"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_COMMUNITY): 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
|
||||
})
|
||||
|
||||
@@ -46,10 +52,20 @@ class SnmpScanner(object):
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
from pysnmp.entity import config as cfg
|
||||
self.snmp = cmdgen.CommandGenerator()
|
||||
|
||||
self.host = cmdgen.UdpTransportTarget((config[CONF_HOST], 161))
|
||||
self.community = cmdgen.CommunityData(config[CONF_COMMUNITY])
|
||||
if CONF_AUTHKEY not in config or CONF_PRIVKEY not in config:
|
||||
self.auth = cmdgen.CommunityData(config[CONF_COMMUNITY])
|
||||
else:
|
||||
self.auth = cmdgen.UsmUserData(
|
||||
config[CONF_COMMUNITY],
|
||||
config[CONF_AUTHKEY],
|
||||
config[CONF_PRIVKEY],
|
||||
authProtocol=cfg.usmHMACSHAAuthProtocol,
|
||||
privProtocol=cfg.usmAesCfb128Protocol
|
||||
)
|
||||
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
|
||||
|
||||
self.lock = threading.Lock()
|
||||
@@ -95,7 +111,7 @@ class SnmpScanner(object):
|
||||
devices = []
|
||||
|
||||
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
|
||||
self.community, self.host, self.baseoid)
|
||||
self.auth, self.host, self.baseoid)
|
||||
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
|
||||
@@ -37,7 +37,6 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class UbusDeviceScanner(object):
|
||||
"""
|
||||
This class queries a wireless router running OpenWrt firmware.
|
||||
|
||||
@@ -7,13 +7,12 @@ https://home-assistant.io/components/device_tracker.volvooncall/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urljoin
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
@@ -26,10 +25,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_URL = 'https://vocapi.wirelesscar.net/customerapi/rest/v3.0/'
|
||||
HEADERS = {"X-Device-Id": "Device",
|
||||
"X-OS-Type": "Android",
|
||||
"X-Originator-Type": "App"}
|
||||
REQUIREMENTS = ['volvooncall==0.1.1']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
@@ -39,62 +35,62 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Validate the configuration and return a scanner."""
|
||||
session = requests.Session()
|
||||
session.headers.update(HEADERS)
|
||||
session.auth = (config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD))
|
||||
from volvooncall import Connection
|
||||
connection = Connection(
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD))
|
||||
|
||||
interval = max(MIN_TIME_BETWEEN_SCANS.seconds,
|
||||
config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
|
||||
|
||||
def query(ref, rel=SERVICE_URL):
|
||||
"""Perform a query to the online service."""
|
||||
url = urljoin(rel, ref)
|
||||
_LOGGER.debug("Request for %s", url)
|
||||
res = session.get(url, timeout=15)
|
||||
res.raise_for_status()
|
||||
_LOGGER.debug("Received %s", res.json())
|
||||
return res.json()
|
||||
def _see_vehicle(vehicle):
|
||||
position = vehicle["position"]
|
||||
dev_id = "volvo_" + slugify(vehicle["registrationNumber"])
|
||||
host_name = "%s (%s/%s)" % (
|
||||
vehicle["registrationNumber"],
|
||||
vehicle["vehicleType"],
|
||||
vehicle["modelYear"])
|
||||
|
||||
def any_opened(door):
|
||||
"""True if any door/window is opened."""
|
||||
return any([door[key] for key in door if "Open" in key])
|
||||
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=(position["latitude"],
|
||||
position["longitude"]),
|
||||
attributes=dict(
|
||||
unlocked=not vehicle["carLocked"],
|
||||
tank_volume=vehicle["fuelTankVolume"],
|
||||
average_fuel_consumption=round(
|
||||
vehicle["averageFuelConsumption"] / 10, 1), # l/100km
|
||||
washer_fluid_low=vehicle["washerFluidLevel"] != "Normal",
|
||||
brake_fluid_low=vehicle["brakeFluid"] != "Normal",
|
||||
service_warning=vehicle["serviceWarningStatus"] != "Normal",
|
||||
bulb_failures=len(vehicle["bulbFailures"]) > 0,
|
||||
doors_open=any_opened(vehicle["doors"]),
|
||||
windows_open=any_opened(vehicle["windows"]),
|
||||
heater_on=vehicle["heater"]["status"] != "off",
|
||||
fuel=vehicle["fuelAmount"],
|
||||
odometer=round(vehicle["odometer"] / 1000), # km
|
||||
range=vehicle["distanceToEmpty"]))
|
||||
|
||||
def update(now):
|
||||
"""Update status from the online service."""
|
||||
_LOGGER.info("Updating")
|
||||
try:
|
||||
_LOGGER.debug("Updating")
|
||||
status = query("status", vehicle_url)
|
||||
position = query("position", vehicle_url)
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=(position["position"]["latitude"],
|
||||
position["position"]["longitude"]),
|
||||
attributes=dict(
|
||||
tank_volume=attributes["fuelTankVolume"],
|
||||
washer_fluid=status["washerFluidLevel"],
|
||||
brake_fluid=status["brakeFluid"],
|
||||
service_warning=status["serviceWarningStatus"],
|
||||
fuel=status["fuelAmount"],
|
||||
odometer=status["odometer"],
|
||||
range=status["distanceToEmpty"]))
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error("Could not query server: %s", error)
|
||||
res, vehicles = connection.update()
|
||||
if not res:
|
||||
_LOGGER.error("Could not query server")
|
||||
return False
|
||||
|
||||
for vehicle in vehicles:
|
||||
_see_vehicle(vehicle)
|
||||
|
||||
return True
|
||||
finally:
|
||||
track_point_in_utc_time(hass, update,
|
||||
now + timedelta(seconds=interval))
|
||||
|
||||
try:
|
||||
_LOGGER.info('Logging in to service')
|
||||
user = query("customeraccounts")
|
||||
rel = query(user["accountVehicleRelations"][0])
|
||||
vehicle_url = rel["vehicle"] + '/'
|
||||
attributes = query("attributes", vehicle_url)
|
||||
|
||||
dev_id = "volvo_" + attributes["registrationNumber"]
|
||||
host_name = "%s %s/%s" % (attributes["registrationNumber"],
|
||||
attributes["vehicleType"],
|
||||
attributes["modelYear"])
|
||||
update(utcnow())
|
||||
return True
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error("Could not log in to service. "
|
||||
"Please check configuration: "
|
||||
"%s", error)
|
||||
return False
|
||||
_LOGGER.info('Logging in to service')
|
||||
return update(utcnow())
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-digitalocean==1.9.0']
|
||||
REQUIREMENTS = ['python-digitalocean==1.10.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,9 +42,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument,too-few-public-methods
|
||||
def setup(hass, config):
|
||||
"""Setup the Digital Ocean component."""
|
||||
"""Set up the Digital Ocean component."""
|
||||
conf = config[DOMAIN]
|
||||
access_token = conf.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
|
||||
@@ -14,15 +14,17 @@ import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
REQUIREMENTS = ['netdisco==0.7.1']
|
||||
REQUIREMENTS = ['netdisco==0.7.5']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HASS_IOS_APP = 'hass_ios'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
SERVICE_NETGEAR: ('device_tracker', None),
|
||||
SERVICE_WEMO: ('wemo', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
@@ -31,6 +33,7 @@ SERVICE_HANDLERS = {
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
'roku': ('media_player', 'roku'),
|
||||
'sonos': ('media_player', 'sonos'),
|
||||
'yamaha': ('media_player', 'yamaha'),
|
||||
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
||||
'directv': ('media_player', 'directv'),
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
"""Listen for download events to download files."""
|
||||
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
|
||||
|
||||
@@ -32,7 +32,6 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def setup(hass, config):
|
||||
"""Setup the Dweet.io component."""
|
||||
conf = config[DOMAIN]
|
||||
|
||||
@@ -86,7 +86,6 @@ def setup_ecobee(hass, network, config):
|
||||
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class EcobeeData(object):
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
|
||||
91
homeassistant/components/emoncms_history.py
Normal file
91
homeassistant/components/emoncms_history.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
A component which allows you to send data to Emoncms.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/emoncms_history/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_WHITELIST, CONF_URL, STATE_UNKNOWN, STATE_UNAVAILABLE,
|
||||
CONF_SCAN_INTERVAL)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import state as state_helper
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'emoncms_history'
|
||||
CONF_INPUTNODE = 'inputnode'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Required(CONF_INPUTNODE): cv.positive_int,
|
||||
vol.Required(CONF_WHITELIST): cv.entity_ids,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=30): cv.positive_int,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Emoncms history component."""
|
||||
conf = config[DOMAIN]
|
||||
whitelist = conf.get(CONF_WHITELIST)
|
||||
|
||||
def send_data(url, apikey, node, payload):
|
||||
"""Send payload data to Emoncms."""
|
||||
try:
|
||||
fullurl = '{}/input/post.json'.format(url)
|
||||
data = {"apikey": apikey, "data": payload}
|
||||
parameters = {"node": node}
|
||||
req = requests.post(
|
||||
fullurl, params=parameters, data=data, allow_redirects=True,
|
||||
timeout=5)
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error("Error saving data '%s' to '%s'",
|
||||
payload, fullurl)
|
||||
|
||||
else:
|
||||
if req.status_code != 200:
|
||||
_LOGGER.error("Error saving data '%s' to '%s'" +
|
||||
"(http status code = %d)", payload,
|
||||
fullurl, req.status_code)
|
||||
|
||||
def update_emoncms(time):
|
||||
"""Send whitelisted entities states reguarly to Emoncms."""
|
||||
payload_dict = {}
|
||||
|
||||
for entity_id in whitelist:
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if state is None or state.state in (
|
||||
STATE_UNKNOWN, '', STATE_UNAVAILABLE):
|
||||
continue
|
||||
|
||||
try:
|
||||
payload_dict[entity_id] = state_helper.state_as_number(
|
||||
state)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if len(payload_dict) > 0:
|
||||
payload = "{%s}" % ",".join("{}:{}".format(key, val)
|
||||
for key, val in
|
||||
payload_dict.items())
|
||||
|
||||
send_data(conf.get(CONF_URL), conf.get(CONF_API_KEY),
|
||||
str(conf.get(CONF_INPUTNODE)), payload)
|
||||
|
||||
track_point_in_time(hass, update_emoncms, time +
|
||||
timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)))
|
||||
|
||||
update_emoncms(dt_util.utcnow())
|
||||
return True
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user