Compare commits
334 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b6a94b0f5 | |||
| 0a333230c1 | |||
| 455e1df7cb | |||
| f71396c293 | |||
| d930c399fe | |||
| f3748ce535 | |||
| 8beefcfc69 | |||
| 93747f2766 | |||
| 7af438fa2f | |||
| 2b5fcd737b | |||
| 2b320f23fc | |||
| 679d500e61 | |||
| 613615433a | |||
| f70ff66d11 | |||
| d2bbc6ef70 | |||
| 37e28428c1 | |||
| c56f99baaf | |||
| 265232af98 | |||
| e6c4113c5b | |||
| c86e1b31b3 | |||
| 5912316496 | |||
| 58f0655298 | |||
| 43a93fb345 | |||
| 36b338051b | |||
| fc566309c1 | |||
| 23ce9949b1 | |||
| 275c80183c | |||
| cd1655f43b | |||
| 1a117d0bea | |||
| 944bb8474f | |||
| 779f520c56 | |||
| 82ed7b6b08 | |||
| af77341494 | |||
| 23fb8c4cdd | |||
| 726bc5b670 | |||
| b615b3349f | |||
| 0f59bb208c | |||
| 38d201a54a | |||
| c8bc1e3c5d | |||
| a862bc4edc | |||
| b0e3d5a576 | |||
| f006b00dc1 | |||
| 1fff6ce438 | |||
| c06c82905a | |||
| 2b86d89bb4 | |||
| 7bdb79bd54 | |||
| 41aaeb715a | |||
| 5d8a465c18 | |||
| c6f5a5443f | |||
| d6cb102f63 | |||
| edde76e544 | |||
| d5fff2f94a | |||
| 0e0ba28249 | |||
| 6745e83a6c | |||
| 44bc057fdb | |||
| 96b8d8fcfa | |||
| fc2df34206 | |||
| 09c29737de | |||
| 4c01b47945 | |||
| 7aaf3a46db | |||
| d774ba46c7 | |||
| 4c37ee8884 | |||
| 7f5f458074 | |||
| 479457d6ec | |||
| 7e73d27dd1 | |||
| e7ffec87ac | |||
| 2d47b187c5 | |||
| fe2103dedb | |||
| 7bf5d1c662 | |||
| cb24282040 | |||
| bd9429d3af | |||
| d7a005ad0f | |||
| 2e2a996a8e | |||
| 0364498dee | |||
| c5fdd4392a | |||
| 895454b6c3 | |||
| 2109b7a1b9 | |||
| 71a305ea45 | |||
| e73634e6c7 | |||
| 3d47ad5018 | |||
| c823ea9f2a | |||
| 75bcb1ff0f | |||
| 1663cc9084 | |||
| 17cfcc981d | |||
| 60fabaec24 | |||
| 5e44934e7e | |||
| 01a6c1c1c8 | |||
| cd1b0ac67d | |||
| 2bfded7153 | |||
| 20af5cb5b4 | |||
| 080f56e0f5 | |||
| 173e15e733 | |||
| 72407c2f95 | |||
| 1b79722b69 | |||
| cc5233103c | |||
| 2feea1d1eb | |||
| 2c39c39d52 | |||
| 6e6b1ef7ab | |||
| 55ddaf1ee7 | |||
| 6860d9b096 | |||
| 3e1cc4282e | |||
| 200bdb30ff | |||
| eb17ba970c | |||
| ffe4c425af | |||
| a18fdbfbb8 | |||
| 58600f25b3 | |||
| 749fc583ea | |||
| b07d887d77 | |||
| 9bb94a4512 | |||
| e76d553513 | |||
| 844799a1f7 | |||
| e005ebe989 | |||
| e9d19c1dcc | |||
| 7d2ab4fce6 | |||
| ba2ea35089 | |||
| ade62faa38 | |||
| ee322dbbdc | |||
| 0d4141bf13 | |||
| d404ac8978 | |||
| 71da21dcc8 | |||
| 04dbc992ec | |||
| 6d0e08cf7d | |||
| 1e0025acae | |||
| 8fc853ba11 | |||
| 8cbb8f6527 | |||
| 4f86c9ecda | |||
| 9561fed650 | |||
| 67b599475e | |||
| 114ece1848 | |||
| c05815cced | |||
| 2e0c185740 | |||
| 231ef40f53 | |||
| b4159c7dc9 | |||
| 8cc5fc1369 | |||
| fc3235fb6d | |||
| d129df93dd | |||
| 67336a111b | |||
| 0af1a96f14 | |||
| 272899ec96 | |||
| 7d28d9d6b4 | |||
| 6a92e27e2f | |||
| faceb4c1dc | |||
| 6d5f00098a | |||
| 618a86a37c | |||
| 880ef8af48 | |||
| 95124c7ddb | |||
| 0aba227300 | |||
| 734bd75fd3 | |||
| 0c5e077091 | |||
| 1ed2f8ae91 | |||
| a343c20404 | |||
| d4e8b831a0 | |||
| 98f41d6b84 | |||
| 7774a03a55 | |||
| c35e5c9997 | |||
| 5d862e426e | |||
| bab8d574fe | |||
| c980d26aae | |||
| 08f75f7935 | |||
| 1ad14b8227 | |||
| 382ac5c3b5 | |||
| af297aa0dc | |||
| 20e1b3eae0 | |||
| 28861221ae | |||
| f367c49fb9 | |||
| ad8645baf4 | |||
| 62785c2431 | |||
| 22c3d014aa | |||
| 3f3127a290 | |||
| 88fc64c8a0 | |||
| 1463fc4fe0 | |||
| ece58ce78f | |||
| b67f1fed52 | |||
| 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 |
+30
-14
@@ -28,6 +28,9 @@ omit =
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
@@ -37,6 +40,9 @@ omit =
|
||||
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,11 +101,12 @@ omit =
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/switch/pilight.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
@@ -109,6 +116,9 @@ omit =
|
||||
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
|
||||
@@ -128,6 +138,7 @@ omit =
|
||||
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
|
||||
@@ -136,16 +147,17 @@ omit =
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/bbox.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.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/cisco_ios.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
@@ -157,8 +169,6 @@ omit =
|
||||
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
|
||||
@@ -172,12 +182,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
|
||||
@@ -188,6 +200,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
|
||||
@@ -209,6 +222,7 @@ omit =
|
||||
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
|
||||
@@ -234,9 +248,12 @@ omit =
|
||||
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
|
||||
@@ -250,9 +267,11 @@ omit =
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hddtemp.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/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
|
||||
@@ -267,7 +286,7 @@ 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/pvoutput.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
@@ -277,6 +296,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
|
||||
@@ -286,7 +306,6 @@ 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/yweather.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
@@ -296,21 +315,18 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
python:
|
||||
enabled: true
|
||||
+3
-3
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
+277
-110
@@ -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 collections import OrderedDict
|
||||
|
||||
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,37 +27,50 @@ 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_PLATFORMS = set()
|
||||
_PERSISTENT_VALIDATION = set()
|
||||
_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)
|
||||
config = {}
|
||||
|
||||
components = loader.load_order_component(domain)
|
||||
|
||||
# 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
|
||||
@@ -64,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
|
||||
|
||||
@@ -72,70 +89,109 @@ 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
|
||||
_LOGGER.info("Setting up %s", domain)
|
||||
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()
|
||||
|
||||
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', [])
|
||||
@@ -151,7 +207,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, hass)
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
return None
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
@@ -161,7 +217,7 @@ 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, hass)
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
continue
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
@@ -171,8 +227,8 @@ 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:
|
||||
continue
|
||||
@@ -180,10 +236,11 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
# 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, hass)
|
||||
async_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated, hass)
|
||||
continue
|
||||
|
||||
platforms.append(p_validated)
|
||||
@@ -195,7 +252,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
|
||||
@@ -204,7 +263,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)
|
||||
|
||||
@@ -213,13 +287,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)
|
||||
|
||||
_PERSISTENT_PLATFORMS.add(platform_path)
|
||||
message = ('Unable to find the following platforms: ' +
|
||||
', '.join(list(_PERSISTENT_PLATFORMS)) +
|
||||
'(please check your configuration)')
|
||||
persistent_notification.create(
|
||||
hass, message, 'Invalid platforms', 'platform_errors')
|
||||
_async_persistent_notification(hass, platform_path)
|
||||
return None
|
||||
|
||||
# Already loaded
|
||||
@@ -228,20 +296,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,
|
||||
@@ -261,15 +332,55 @@ 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.async_add_job(_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.
|
||||
"""
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
yield from setup_lock.acquire()
|
||||
|
||||
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, hass)
|
||||
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)
|
||||
@@ -279,41 +390,42 @@ 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
|
||||
# Use OrderedDict in case original one was one.
|
||||
# Convert values to dictionaries if they are None
|
||||
config = defaultdict(
|
||||
dict, {key: value or {} for key, value in config.items()})
|
||||
new_config = OrderedDict()
|
||||
for key, value in config.items():
|
||||
new_config[key] = value or {}
|
||||
config = new_config
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
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)
|
||||
)
|
||||
setup_lock.release()
|
||||
|
||||
return hass
|
||||
|
||||
@@ -331,27 +443,62 @@ 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")
|
||||
@@ -359,6 +506,7 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
# 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
|
||||
@@ -406,43 +554,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, hass=None):
|
||||
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:
|
||||
_PERSISTENT_VALIDATION.add(domain)
|
||||
message = ('The following platforms contain invalid configuration: ' +
|
||||
', '.join(list(_PERSISTENT_VALIDATION)) +
|
||||
' (please check your configuration)')
|
||||
persistent_notification.create(
|
||||
hass, message, 'Invalid config', 'invalid_config')
|
||||
_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.wait(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
|
||||
|
||||
@@ -4,23 +4,19 @@ 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)
|
||||
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
|
||||
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -29,17 +25,17 @@ 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,
|
||||
})
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup concord232 platform."""
|
||||
"""Set up the Concord232 alarm control panel platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
@@ -49,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
try:
|
||||
add_devices([Concord232Alarm(hass, url, name)])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to Concord232: %s', str(ex))
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return False
|
||||
|
||||
|
||||
@@ -57,7 +53,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
"""Represents the Concord232-based alarm panel."""
|
||||
|
||||
def __init__(self, hass, url, name):
|
||||
"""Initalize the concord232 alarm panel."""
|
||||
"""Initialize the Concord232 alarm panel."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
self._state = STATE_UNKNOWN
|
||||
@@ -68,7 +64,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
try:
|
||||
client = concord232_client.Client(self._url)
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to Concord232: %s', str(ex))
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
|
||||
self._alarm = client
|
||||
self._alarm.partitions = self._alarm.list_partitions()
|
||||
@@ -100,16 +96,16 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||
_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')
|
||||
_LOGGER.error("Concord232 reports no partitions")
|
||||
newstate = STATE_UNKNOWN
|
||||
|
||||
if part['arming_level'] == "Off":
|
||||
if part['arming_level'] == 'Off':
|
||||
newstate = STATE_ALARM_DISARMED
|
||||
elif "Home" in part['arming_level']:
|
||||
elif 'Home' in part['arming_level']:
|
||||
newstate = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
newstate = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
@@ -4,26 +4,48 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
from os import path
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
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)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
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)
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEVICES = []
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
|
||||
ATTR_KEYPRESS = 'keypress'
|
||||
ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_KEYPRESS): cv.string
|
||||
})
|
||||
|
||||
|
||||
def alarm_keypress_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
keypress = service.data.get(ATTR_KEYPRESS)
|
||||
|
||||
_target_devices = [device for device in DEVICES
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
for device in _target_devices:
|
||||
EnvisalinkAlarm.alarm_keypress(device, keypress)
|
||||
|
||||
|
||||
# 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 +60,39 @@ 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])
|
||||
DEVICES.append(_device)
|
||||
|
||||
add_devices(DEVICES)
|
||||
|
||||
# Register Envisalink specific services
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(alarm.DOMAIN, SERVICE_ALARM_KEYPRESS,
|
||||
alarm_keypress_handler,
|
||||
descriptions.get(SERVICE_ALARM_KEYPRESS),
|
||||
schema=ALARM_KEYPRESS_SCHEMA)
|
||||
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."""
|
||||
@@ -70,42 +101,64 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return self._code
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
else:
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
state = STATE_UNKNOWN
|
||||
|
||||
if self._info['status']['alarm']:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
elif self._info['status']['armed_away']:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif self._info['status']['armed_stay']:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif self._info['status']['exit_delay']:
|
||||
state = STATE_ALARM_PENDING
|
||||
elif self._info['status']['entry_delay']:
|
||||
state = STATE_ALARM_PENDING
|
||||
elif self._info['status']['alpha']:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
state = STATE_ALARM_DISARMED
|
||||
return state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._code:
|
||||
if code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.disarm_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if self._code:
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._code:
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_away_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
||||
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
||||
|
||||
def alarm_keypress(self, keypress=None):
|
||||
"""Send custom keypress."""
|
||||
if keypress:
|
||||
EVL_CONTROLLER.keypresses_to_partition(self._partition_number,
|
||||
keypress)
|
||||
|
||||
@@ -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.
|
||||
@@ -131,7 +129,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
@@ -145,7 +143,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
@@ -157,11 +155,11 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
if self._trigger_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time + self._trigger_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -41,3 +41,14 @@ alarm_trigger:
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
envisalink_alarm_keypress:
|
||||
description: Send custom keypresses to the alarm
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel (1-6 characters)'
|
||||
example: '*71'
|
||||
|
||||
@@ -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,6 +4,7 @@ Support for Alexa skill service end point.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
"""
|
||||
import asyncio
|
||||
import copy
|
||||
import enum
|
||||
import logging
|
||||
@@ -12,6 +13,7 @@ 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
|
||||
@@ -20,7 +22,7 @@ import homeassistant.util.dt as dt_util
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/<briefing_id>'
|
||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
|
||||
|
||||
CONF_ACTION = 'action'
|
||||
CONF_CARD = 'card'
|
||||
@@ -102,8 +104,8 @@ def setup(hass, config):
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
|
||||
|
||||
hass.wsgi.register_view(AlexaIntentsView(hass, intents))
|
||||
hass.wsgi.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
||||
hass.http.register_view(AlexaIntentsView(hass, intents))
|
||||
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
||||
|
||||
return True
|
||||
|
||||
@@ -128,9 +130,10 @@ class AlexaIntentsView(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)
|
||||
|
||||
@@ -176,7 +179,7 @@ class AlexaIntentsView(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:
|
||||
@@ -218,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):
|
||||
@@ -229,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,
|
||||
@@ -244,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):
|
||||
@@ -283,7 +286,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
@callback
|
||||
def get(self, request, briefing_id):
|
||||
"""Handle Alexa Flash Briefing request."""
|
||||
_LOGGER.debug('Received Alexa flash briefing request for: %s',
|
||||
@@ -292,7 +295,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
||||
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 self.Response(status=404)
|
||||
return b'', 404
|
||||
|
||||
briefing = []
|
||||
|
||||
@@ -300,13 +303,13 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
||||
output = {}
|
||||
if item.get(CONF_TITLE) is not None:
|
||||
if isinstance(item.get(CONF_TITLE), template.Template):
|
||||
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].render()
|
||||
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].render()
|
||||
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render()
|
||||
else:
|
||||
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
|
||||
|
||||
@@ -315,7 +318,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
||||
|
||||
if item.get(CONF_AUDIO) is not None:
|
||||
if isinstance(item.get(CONF_AUDIO), template.Template):
|
||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].render()
|
||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render()
|
||||
else:
|
||||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||
|
||||
@@ -323,7 +326,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
||||
if isinstance(item.get(CONF_DISPLAY_URL),
|
||||
template.Template):
|
||||
output[ATTR_REDIRECTION_URL] = \
|
||||
item[CONF_DISPLAY_URL].render()
|
||||
item[CONF_DISPLAY_URL].async_render()
|
||||
else:
|
||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||
|
||||
|
||||
+117
-78
@@ -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,8 +11,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
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,
|
||||
@@ -25,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 + '.{}'
|
||||
@@ -68,6 +66,7 @@ def _platform_validator(config):
|
||||
|
||||
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
||||
|
||||
|
||||
_TRIGGER_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
@@ -144,42 +143,50 @@ 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')
|
||||
)
|
||||
|
||||
@callback
|
||||
@asyncio.coroutine
|
||||
def trigger_service_handler(service_call):
|
||||
"""Handle automation triggers."""
|
||||
tasks = []
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
hass.loop.create_task(entity.async_trigger(
|
||||
tasks.append(entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES), True))
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@callback
|
||||
@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.async_extract_from_service(service_call):
|
||||
hass.loop.create_task(getattr(entity, method)())
|
||||
tasks.append(getattr(entity, method)())
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@callback
|
||||
@asyncio.coroutine
|
||||
def toggle_service_handler(service_call):
|
||||
"""Handle automation toggle service calls."""
|
||||
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.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
@@ -187,24 +194,24 @@ def setup(hass, config):
|
||||
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 +219,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 +265,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,8 +276,6 @@ class AutomationEntity(ToggleEntity):
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
self._enabled = False
|
||||
# It's important that the update is finished before this method
|
||||
# ends because async_remove depends on it.
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -284,7 +287,7 @@ class AutomationEntity(ToggleEntity):
|
||||
if skip_condition or self._cond_func(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()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_remove(self):
|
||||
@@ -346,8 +349,10 @@ def _async_process_config(hass, config, component):
|
||||
tasks.append(entity.async_enable())
|
||||
entities.append(entity)
|
||||
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
hass.loop.create_task(component.async_add_entities(entities))
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
return len(entities) > 0
|
||||
|
||||
@@ -362,7 +367,7 @@ def _async_get_action(hass, config, name):
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.async_log_entry(
|
||||
hass, name, 'has been triggered', DOMAIN, entity_id)
|
||||
hass.loop.create_task(script_obj.async_run(variables))
|
||||
yield from script_obj.async_run(variables)
|
||||
|
||||
return action
|
||||
|
||||
@@ -395,9 +400,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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -5,20 +5,16 @@ 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
|
||||
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -27,9 +23,12 @@ 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),
|
||||
})
|
||||
@@ -42,14 +41,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
||||
})
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
|
||||
DEFAULT_NAME = "Alarm"
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Concord232 binary sensor platform."""
|
||||
"""Set up the Concord232 binary sensor platform."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
@@ -59,24 +53,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
sensors = []
|
||||
|
||||
try:
|
||||
_LOGGER.debug('Initializing Client.')
|
||||
client = concord232_client.Client('http://{}:{}'
|
||||
.format(host, port))
|
||||
_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))
|
||||
_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'])
|
||||
_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))))
|
||||
sensors.append(
|
||||
Concord232ZoneSensor(
|
||||
hass, client, zone, zone_types.get(zone['number'],
|
||||
get_opening_type(zone)))
|
||||
)
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
@@ -84,16 +77,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
def get_opening_type(zone):
|
||||
"""Helper function to try to guess sensor type frm 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"
|
||||
"""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):
|
||||
|
||||
@@ -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
|
||||
@@ -138,7 +138,7 @@ class FFmpegBinarySensor(BinarySensorDevice):
|
||||
def _callback(self, state):
|
||||
"""HA-FFmpeg callback for noise detection."""
|
||||
self._state = state
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _start_ffmpeg(self, config):
|
||||
"""Start a FFmpeg instance."""
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES)
|
||||
@@ -51,7 +52,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."""
|
||||
|
||||
@@ -67,17 +67,18 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
self._payload_off = payload_off
|
||||
self._qos = qos
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._payload_on:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._payload_off:
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
if not gateways:
|
||||
return
|
||||
|
||||
for gateway in gateways:
|
||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||
# states. Map them in a dict of lists.
|
||||
pres = gateway.const.Presentation
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -13,7 +13,7 @@ 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.const import CONF_MONITORED_CONDITIONS, CONF_TIMEOUT
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
@@ -33,6 +33,7 @@ CONF_CAMERAS = 'cameras'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
|
||||
@@ -45,6 +46,7 @@ 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)
|
||||
timeout = config.get(CONF_TIMEOUT, 15)
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
@@ -62,18 +64,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
for variable in sensors:
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, home,
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, home, timeout,
|
||||
variable)])
|
||||
|
||||
|
||||
class WelcomeBinarySensor(BinarySensorDevice):
|
||||
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
||||
|
||||
def __init__(self, data, camera_name, home, sensor):
|
||||
def __init__(self, data, camera_name, home, timeout, sensor):
|
||||
"""Setup for access to the Netatmo camera events."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._home = home
|
||||
self._timeout = timeout
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
@@ -114,14 +117,17 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneKnownSeen(self._home,
|
||||
self._camera_name)
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Someone unknown":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneUnknownSeen(self._home,
|
||||
self._camera_name)
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Motion":
|
||||
self._state =\
|
||||
self._data.welcomedata.motionDetected(self._home,
|
||||
self._camera_name)
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -123,7 +123,7 @@ class NX584Watcher(threading.Thread):
|
||||
if not zone_sensor:
|
||||
return
|
||||
zone_sensor._zone['state'] = event['zone_state']
|
||||
zone_sensor.update_ha_state()
|
||||
zone_sensor.schedule_update_ha_state()
|
||||
|
||||
def _process_events(self, events):
|
||||
for event in events:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -41,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)
|
||||
@@ -76,7 +75,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hass, rest, name, sensor_class, value_template)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a REST binary sensor."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -73,7 +72,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
def read_gpio(port):
|
||||
"""Read state from GPIO."""
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -63,14 +63,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
|
||||
hass.loop.create_task(async_add_devices(sensors))
|
||||
yield from 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."""
|
||||
@@ -82,12 +81,10 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
|
||||
self._async_render()
|
||||
|
||||
@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))
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
async_track_state_change(
|
||||
hass, entity_ids, template_bsensor_state_listener)
|
||||
@@ -115,10 +112,6 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the state from the template."""
|
||||
self._async_render()
|
||||
|
||||
def _async_render(self):
|
||||
"""Render the state from the template."""
|
||||
try:
|
||||
self._state = self._template.async_render().lower() == 'true'
|
||||
except TemplateError as ex:
|
||||
|
||||
@@ -4,8 +4,11 @@ A sensor that monitors trands in other components.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.trend/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -72,7 +75,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."""
|
||||
@@ -88,13 +90,12 @@ class SensorTrend(BinarySensorDevice):
|
||||
self.from_state = None
|
||||
self.to_state = None
|
||||
|
||||
self.update()
|
||||
|
||||
@callback
|
||||
def trend_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.from_state = old_state
|
||||
self.to_state = new_state
|
||||
self.update_ha_state(True)
|
||||
hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
track_state_change(hass, target_entity,
|
||||
trend_sensor_state_listener)
|
||||
@@ -119,7 +120,8 @@ class SensorTrend(BinarySensorDevice):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
if self.from_state is None or self.to_state is None:
|
||||
return
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ class WemoBinarySensor(BinarySensorDevice):
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s',
|
||||
_device)
|
||||
self.update()
|
||||
if not hasattr(self, 'hass'):
|
||||
self.update()
|
||||
return
|
||||
self.update_ha_state(True)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/binary_sensor.wink/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
@@ -53,12 +54,17 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
self.capability = self.wink.capability()
|
||||
|
||||
def _pubnub_update(self, message, channel):
|
||||
if 'data' in message:
|
||||
json_data = json.dumps(message.get('data'))
|
||||
else:
|
||||
json_data = message
|
||||
self.wink.pubnub_update(json.loads(json_data))
|
||||
self.update_ha_state()
|
||||
try:
|
||||
if 'data' in message:
|
||||
json_data = json.dumps(message.get('data'))
|
||||
else:
|
||||
json_data = message
|
||||
self.wink.pubnub_update(json.loads(json_data))
|
||||
self.update_ha_state()
|
||||
except (AttributeError, KeyError):
|
||||
error = "Pubnub returned invalid json for " + self.name
|
||||
logging.getLogger(__name__).error(error)
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -96,7 +96,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
||||
@@ -112,19 +112,19 @@ class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
||||
# If it's active make sure that we set the timeout tracker
|
||||
if sensor_value.data:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
if value.data:
|
||||
# only allow this value to be true for re_arm secs
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@property
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Support for Google Calendar event device sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar/
|
||||
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from homeassistant.components.google import (CONF_OFFSET,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_NAME)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.helpers.config_validation import time_period_str
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.util import dt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'calendar'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
DEFAULT_CONF_TRACK_NEW = True
|
||||
DEFAULT_CONF_OFFSET = '!!'
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class CalendarEventDevice(Entity):
|
||||
"""A calendar event device."""
|
||||
|
||||
# Classes overloading this must set data to an object
|
||||
# with an update() method
|
||||
data = None
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, data):
|
||||
"""Create the Calendar Event Device."""
|
||||
self._name = data.get(CONF_NAME)
|
||||
self.dev_id = data.get(CONF_DEVICE_ID)
|
||||
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT,
|
||||
self.dev_id,
|
||||
hass=hass)
|
||||
|
||||
self._cal_data = {
|
||||
'all_day': False,
|
||||
'offset_time': dt.dt.timedelta(),
|
||||
'message': '',
|
||||
'start': None,
|
||||
'end': None,
|
||||
'location': '',
|
||||
'description': '',
|
||||
}
|
||||
|
||||
self.update()
|
||||
|
||||
def offset_reached(self):
|
||||
"""Have we reached the offset time specified in the event title."""
|
||||
if self._cal_data['start'] is None or \
|
||||
self._cal_data['offset_time'] == dt.dt.timedelta():
|
||||
return False
|
||||
|
||||
return self._cal_data['start'] + self._cal_data['offset_time'] <= \
|
||||
dt.now(self._cal_data['start'].tzinfo)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""State Attributes for HA."""
|
||||
start = self._cal_data.get('start', None)
|
||||
end = self._cal_data.get('end', None)
|
||||
start = start.strftime(DATE_STR_FORMAT) if start is not None else None
|
||||
end = end.strftime(DATE_STR_FORMAT) if end is not None else None
|
||||
|
||||
return {
|
||||
'message': self._cal_data.get('message', ''),
|
||||
'all_day': self._cal_data.get('all_day', False),
|
||||
'offset_reached': self.offset_reached(),
|
||||
'start_time': start,
|
||||
'end_time': end,
|
||||
'location': self._cal_data.get('location', None),
|
||||
'description': self._cal_data.get('description', None),
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the calendar event."""
|
||||
start = self._cal_data.get('start', None)
|
||||
end = self._cal_data.get('end', None)
|
||||
if start is None or end is None:
|
||||
return STATE_OFF
|
||||
|
||||
now = dt.now()
|
||||
|
||||
if start <= now and end > now:
|
||||
return STATE_ON
|
||||
|
||||
if now >= end:
|
||||
self.cleanup()
|
||||
|
||||
return STATE_OFF
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup any start/end listeners that were setup."""
|
||||
self._cal_data = {
|
||||
'all_day': False,
|
||||
'offset_time': 0,
|
||||
'message': '',
|
||||
'start': None,
|
||||
'end': None,
|
||||
'location': None,
|
||||
'description': None
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Search for the next event."""
|
||||
if not self.data or not self.data.update():
|
||||
# update cached, don't do anything
|
||||
return
|
||||
|
||||
if not self.data.event:
|
||||
# we have no event to work on, make sure we're clean
|
||||
self.cleanup()
|
||||
return
|
||||
|
||||
def _get_date(date):
|
||||
"""Get the dateTime from date or dateTime as a local."""
|
||||
if 'date' in date:
|
||||
return dt.as_utc(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time()))
|
||||
else:
|
||||
return dt.parse_datetime(date['dateTime'])
|
||||
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
||||
summary = self.data.event['summary']
|
||||
|
||||
# check if we have an offset tag in the message
|
||||
# time is HH:MM or MM
|
||||
reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(self._offset)
|
||||
search = re.search(reg, summary)
|
||||
if search and search.group(1):
|
||||
time = search.group(1)
|
||||
if ':' not in time:
|
||||
if time[0] == '+' or time[0] == '-':
|
||||
time = '{}0:{}'.format(time[0], time[1:])
|
||||
else:
|
||||
time = '0:{}'.format(time)
|
||||
|
||||
offset_time = time_period_str(time)
|
||||
summary = (summary[:search.start()] + summary[search.end():]) \
|
||||
.strip()
|
||||
else:
|
||||
offset_time = dt.dt.timedelta() # default it
|
||||
|
||||
# cleanup the string so we don't have a bunch of double+ spaces
|
||||
self._cal_data['message'] = re.sub(' +', '', summary).strip()
|
||||
|
||||
self._cal_data['offset_time'] = offset_time
|
||||
self._cal_data['location'] = self.data.event.get('location', '')
|
||||
self._cal_data['description'] = self.data.event.get('description', '')
|
||||
self._cal_data['start'] = start
|
||||
self._cal_data['end'] = end
|
||||
self._cal_data['all_day'] = 'date' in self.data.event['start']
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Demo platform that has two fake binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo binary sensor platform."""
|
||||
calendar_data_future = DemoGoogleCalendarDataFuture()
|
||||
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
||||
add_devices([
|
||||
DemoGoogleCalendar(hass, calendar_data_future, {
|
||||
CONF_NAME: 'Future Event',
|
||||
CONF_DEVICE_ID: 'future_event',
|
||||
}),
|
||||
|
||||
DemoGoogleCalendar(hass, calendar_data_current, {
|
||||
CONF_NAME: 'Current Event',
|
||||
CONF_DEVICE_ID: 'current_event',
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
class DemoGoogleCalendarData(object):
|
||||
"""Setup base class for data."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def update(self):
|
||||
"""Return true so entity knows we have new data."""
|
||||
return True
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
||||
"""Setup future data event."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event to a future event."""
|
||||
one_hour_from_now = dt_util.now() \
|
||||
+ dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': one_hour_from_now.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (one_hour_from_now + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Future Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
|
||||
"""Create a current event we're in the middle of."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event data."""
|
||||
middle_of_event = dt_util.now() \
|
||||
- dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': middle_of_event.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (middle_of_event + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Current Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendar(CalendarEventDevice):
|
||||
"""A Demo binary sensor."""
|
||||
|
||||
def __init__(self, hass, calendar_data, data):
|
||||
"""The same as a google calendar but without the api calls."""
|
||||
self.data = calendar_data
|
||||
super().__init__(hass, data)
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Support for Google Calendar Search binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.google_calendar/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import (CONF_CAL_ID, CONF_ENTITIES,
|
||||
CONF_TRACK, TOKEN_FILE,
|
||||
GoogleCalendarService)
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
||||
'orderBy': 'startTime',
|
||||
'maxResults': 1,
|
||||
'singleEvents': True,
|
||||
}
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
"""Setup the calendar platform for event devices."""
|
||||
if disc_info is None:
|
||||
return
|
||||
|
||||
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
||||
return
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
|
||||
disc_info[CONF_CAL_ID], data)
|
||||
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
"""A calendar event device."""
|
||||
|
||||
def __init__(self, hass, calendar_service, calendar, data):
|
||||
"""Create the Calendar event device."""
|
||||
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||
data.get('search', None))
|
||||
super().__init__(hass, data)
|
||||
|
||||
|
||||
class GoogleCalendarData(object):
|
||||
"""Class to utilize calendar service object to get next event."""
|
||||
|
||||
def __init__(self, calendar_service, calendar_id, search=None):
|
||||
"""Setup how we are going to search the google calendar."""
|
||||
self.calendar_service = calendar_service
|
||||
self.calendar_id = calendar_id
|
||||
self.search = search
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.utcnow().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
if self.search:
|
||||
params['q'] = self.search
|
||||
|
||||
events = service.events() # pylint: disable=no-member
|
||||
result = events.list(**params).execute()
|
||||
|
||||
items = result.get('items', [])
|
||||
self.event = items[0] if len(items) == 1 else None
|
||||
return True
|
||||
@@ -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)])
|
||||
yield from 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.async_add_job(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)])
|
||||
yield from 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)])
|
||||
yield from 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.async_add_job(stream.release())
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -4,28 +4,32 @@ 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 requests
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST)
|
||||
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__)
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
CONF_VALID_CERT = 'valid_cert'
|
||||
|
||||
QUERY_CGI = 'query.cgi'
|
||||
QUERY_API = 'SYNO.API.Info'
|
||||
@@ -38,6 +42,7 @@ 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}'
|
||||
|
||||
@@ -47,175 +52,249 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_VALID_CERT, default=True): cv.boolean,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
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 Synology IP Camera."""
|
||||
if not config.get(CONF_VERIFY_SSL):
|
||||
connector = aiohttp.TCPConnector(verify_ssl=False)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_close_connector(event):
|
||||
"""Close websession on shutdown."""
|
||||
yield from connector.close()
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _async_close_connector)
|
||||
else:
|
||||
connector = hass.websession.connector
|
||||
|
||||
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.'}
|
||||
query_req = requests.get(syno_api_url,
|
||||
params=query_payload,
|
||||
verify=config.get(CONF_VALID_CERT),
|
||||
timeout=TIMEOUT)
|
||||
query_resp = query_req.json()
|
||||
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 = get_session_id(config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url,
|
||||
config.get(CONF_VALID_CERT))
|
||||
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})
|
||||
|
||||
@callback
|
||||
def _async_close_websession(event):
|
||||
"""Close websession on shutdown."""
|
||||
websession.detach()
|
||||
|
||||
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'}
|
||||
camera_req = requests.get(syno_camera_url,
|
||||
params=camera_payload,
|
||||
verify=config.get(CONF_VALID_CERT),
|
||||
timeout=TIMEOUT,
|
||||
cookies={'id': session_id})
|
||||
camera_resp = camera_req.json()
|
||||
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']
|
||||
|
||||
add_devices([SynologyCamera(config,
|
||||
camera_id,
|
||||
camera['name'],
|
||||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_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)
|
||||
|
||||
|
||||
def get_session_id(username, password, login_url, valid_cert):
|
||||
@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'}
|
||||
auth_req = requests.get(login_url,
|
||||
params=auth_payload,
|
||||
verify=valid_cert,
|
||||
timeout=TIMEOUT)
|
||||
auth_resp = auth_req.json()
|
||||
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']
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, config, camera_id, camera_name,
|
||||
snapshot_path, streaming_path, camera_path, auth_path):
|
||||
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._username = config.get(CONF_USERNAME)
|
||||
self._password = config.get(CONF_PASSWORD)
|
||||
self._synology_url = config.get(CONF_URL)
|
||||
self._api_url = config.get(CONF_URL) + 'webapi/'
|
||||
self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi'
|
||||
self._camera_name = config.get(CONF_CAMERA_NAME)
|
||||
self._stream_id = config.get(CONF_STREAM_ID)
|
||||
self._valid_cert = config.get(CONF_VALID_CERT)
|
||||
self._camera_id = camera_id
|
||||
self._snapshot_path = snapshot_path
|
||||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
|
||||
self._session_id = get_session_id(self._username,
|
||||
self._password,
|
||||
self._login_url,
|
||||
self._valid_cert)
|
||||
|
||||
def get_sid(self):
|
||||
"""Get a session id."""
|
||||
auth_payload = {'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': self._username,
|
||||
'passwd': self._password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'}
|
||||
auth_req = requests.get(self._login_url,
|
||||
params=auth_payload,
|
||||
verify=self._valid_cert,
|
||||
timeout=TIMEOUT)
|
||||
auth_resp = auth_req.json()
|
||||
self._session_id = auth_resp['data']['sid']
|
||||
|
||||
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}
|
||||
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:
|
||||
response = requests.get(image_url,
|
||||
params=image_payload,
|
||||
timeout=TIMEOUT,
|
||||
verify=self._valid_cert,
|
||||
cookies={'id': self._session_id})
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
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
|
||||
|
||||
return response.content
|
||||
image = yield from response.read()
|
||||
yield from response.release()
|
||||
|
||||
def camera_stream(self):
|
||||
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'}
|
||||
response = requests.get(streaming_url,
|
||||
payload=streaming_payload,
|
||||
stream=True,
|
||||
timeout=TIMEOUT,
|
||||
cookies={'id': self._session_id})
|
||||
return response
|
||||
streaming_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
||||
|
||||
def mjpeg_steam(self, response):
|
||||
"""Generate an HTTP MJPEG Stream from the Synology NAS."""
|
||||
stream = self.camera_stream()
|
||||
return response(
|
||||
stream.iter_content(chunk_size=1024),
|
||||
mimetype=stream.headers['CONTENT_TYPE_HEADER'],
|
||||
direct_passthrough=True
|
||||
)
|
||||
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):
|
||||
|
||||
@@ -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)
|
||||
@@ -364,7 +362,7 @@ 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."""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ 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 eQ-3 Bluetooth Smart thermostat."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
@@ -147,7 +145,7 @@ class GenericThermostat(ClimateDevice):
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
@@ -160,7 +158,7 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
self._update_temp(new_state)
|
||||
self._control_heating()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
|
||||
@@ -56,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
|
||||
|
||||
@@ -36,7 +36,6 @@ 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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -197,7 +196,6 @@ class RoundThermostat(ClimateDevice):
|
||||
self._is_dhw = False
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Representation of a Honeywell US Thermostat."""
|
||||
|
||||
@@ -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):
|
||||
@@ -70,16 +72,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
@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'))
|
||||
|
||||
@@ -24,7 +24,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors climate."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
if not gateways:
|
||||
return
|
||||
|
||||
for gateway in gateways:
|
||||
if float(gateway.protocol_version) < 1.5:
|
||||
continue
|
||||
pres = gateway.const.Presentation
|
||||
@@ -37,7 +42,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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -54,18 +58,33 @@ class NestThermostat(ClimateDevice):
|
||||
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 temperature_unit(self):
|
||||
@@ -75,11 +94,11 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
if self.device.has_humidifier or self.device.has_dehumidifier:
|
||||
if self._has_humidifier or self._has_dehumidifier:
|
||||
# Move these to Thermostat Device and make them global
|
||||
return {
|
||||
"humidity": self.device.humidity,
|
||||
"target_humidity": self.device.target_humidity,
|
||||
"humidity": self._humidity,
|
||||
"target_humidity": self._target_humidity,
|
||||
}
|
||||
else:
|
||||
# No way to control humidity not show setting
|
||||
@@ -88,18 +107,18 @@ class NestThermostat(ClimateDevice):
|
||||
@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
|
||||
@@ -107,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."""
|
||||
@@ -145,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)
|
||||
@@ -179,9 +198,9 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
if self.device.has_fan:
|
||||
if self._has_fan:
|
||||
# Return whether the fan is on
|
||||
return STATE_ON if self.device.fan else STATE_AUTO
|
||||
return STATE_ON if self._fan else STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
@@ -198,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:
|
||||
@@ -207,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
|
||||
|
||||
@@ -54,7 +54,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
return None
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class NetatmoThermostat(ClimateDevice):
|
||||
"""Representation a Netatmo thermostat."""
|
||||
|
||||
|
||||
@@ -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.4.0']
|
||||
REQUIREMENTS = ['proliphix==0.4.1']
|
||||
|
||||
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."""
|
||||
|
||||
|
||||
@@ -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]
|
||||
@@ -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."""
|
||||
|
||||
@@ -95,7 +97,13 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
@property
|
||||
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):
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Support for Wink thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.wink/
|
||||
"""
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_CURRENT_HUMIDITY)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, STATE_ON,
|
||||
STATE_OFF, STATE_UNKNOWN)
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
STATE_AUX = 'aux'
|
||||
STATE_ECO = 'eco'
|
||||
|
||||
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
ATTR_SMART_TEMPERATURE = "smart_temperature"
|
||||
ATTR_ECO_TARGET = "eco_target"
|
||||
ATTR_OCCUPIED = "occupied"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink thermostat."""
|
||||
import pywink
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
add_devices(WinkThermostat(thermostat, temp_unit)
|
||||
for thermostat in pywink.get_thermostats())
|
||||
|
||||
|
||||
# pylint: disable=abstract-method,too-many-public-methods, too-many-branches
|
||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink thermostat."""
|
||||
|
||||
def __init__(self, wink, temp_unit):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink)
|
||||
wink = get_component('wink')
|
||||
self._config_temp_unit = temp_unit
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
# The Wink API always returns temp in Celsius
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {}
|
||||
target_temp_high = self.target_temperature_high
|
||||
target_temp_low = self.target_temperature_low
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
if target_temp_low is not None:
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
|
||||
if self.external_temperature:
|
||||
data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display(
|
||||
self.external_temperature)
|
||||
|
||||
if self.smart_temperature:
|
||||
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
|
||||
|
||||
if self.occupied:
|
||||
data[ATTR_OCCUPIED] = self.occupied
|
||||
|
||||
if self.eco_target:
|
||||
data[ATTR_ECO_TARGET] = self.eco_target
|
||||
|
||||
current_humidity = self.current_humidity
|
||||
if current_humidity is not None:
|
||||
data[ATTR_CURRENT_HUMIDITY] = current_humidity
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.wink.current_temperature()
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
if self.wink.current_humidity() is not None:
|
||||
# The API states humidity will be a float 0-1
|
||||
# the only example API response with humidity listed show an int
|
||||
# This will address both possibilities
|
||||
if self.wink.current_humidity() < 1:
|
||||
return self.wink.current_humidity() * 100
|
||||
else:
|
||||
return self.wink.current_humidity()
|
||||
|
||||
@property
|
||||
def external_temperature(self):
|
||||
"""Return the current external temperature."""
|
||||
return self.wink.current_external_temperature()
|
||||
|
||||
@property
|
||||
def smart_temperature(self):
|
||||
"""Return the current average temp of all remote sensor."""
|
||||
return self.wink.current_smart_temperature()
|
||||
|
||||
@property
|
||||
def eco_target(self):
|
||||
"""Return status of eco target (Is the termostat in eco mode)."""
|
||||
return self.wink.eco_target()
|
||||
|
||||
@property
|
||||
def occupied(self):
|
||||
"""Return status of if the thermostat has detected occupancy."""
|
||||
return self.wink.occupied()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
elif self.wink.current_hvac_mode() == 'cool_only':
|
||||
current_op = STATE_COOL
|
||||
elif self.wink.current_hvac_mode() == 'heat_only':
|
||||
current_op = STATE_HEAT
|
||||
elif self.wink.current_hvac_mode() == 'aux':
|
||||
current_op = STATE_HEAT
|
||||
elif self.wink.current_hvac_mode() == 'auto':
|
||||
current_op = STATE_AUTO
|
||||
elif self.wink.current_hvac_mode() == 'eco':
|
||||
current_op = STATE_ECO
|
||||
else:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
|
||||
@property
|
||||
def target_humidity(self):
|
||||
"""Return the humidity we try to reach."""
|
||||
target_hum = None
|
||||
if self.wink.current_humidifier_mode() == 'on':
|
||||
if self.wink.current_humidifier_set_point() is not None:
|
||||
target_hum = self.wink.current_humidifier_set_point() * 100
|
||||
elif self.wink.current_dehumidifier_mode() == 'on':
|
||||
if self.wink.current_dehumidifier_set_point() is not None:
|
||||
target_hum = self.wink.current_dehumidifier_set_point() * 100
|
||||
else:
|
||||
target_hum = None
|
||||
return target_hum
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.current_operation != STATE_AUTO and not self.is_away_mode_on:
|
||||
if self.current_operation == STATE_COOL:
|
||||
return self.wink.current_max_set_point()
|
||||
elif self.current_operation == STATE_HEAT:
|
||||
return self.wink.current_min_set_point()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return self.wink.current_min_set_point()
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the higher bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return self.wink.current_max_set_point()
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if away mode is on."""
|
||||
return self.wink.away()
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if aux heater."""
|
||||
if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on():
|
||||
return True
|
||||
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp is not None:
|
||||
if self.current_operation == STATE_COOL:
|
||||
target_temp_high = target_temp
|
||||
if self.current_operation == STATE_HEAT:
|
||||
target_temp_low = target_temp
|
||||
if target_temp_low is not None:
|
||||
target_temp_low = target_temp_low
|
||||
if target_temp_high is not None:
|
||||
target_temp_high = target_temp_high
|
||||
self.wink.set_temperature(target_temp_low, target_temp_high)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.wink.set_operation_mode('heat_only')
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.wink.set_operation_mode('cool_only')
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.wink.set_operation_mode('auto')
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.wink.set_operation_mode('off')
|
||||
elif operation_mode == STATE_AUX:
|
||||
self.wink.set_operation_mode('aux')
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.wink.set_operation_mode('eco')
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
op_list = ['off']
|
||||
modes = self.wink.hvac_modes()
|
||||
if 'cool_only' in modes:
|
||||
op_list.append(STATE_COOL)
|
||||
if 'heat_only' in modes or 'aux' in modes:
|
||||
op_list.append(STATE_HEAT)
|
||||
if 'auto' in modes:
|
||||
op_list.append(STATE_AUTO)
|
||||
if 'eco' in modes:
|
||||
op_list.append(STATE_ECO)
|
||||
return op_list
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.wink.set_away_mode()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.wink.set_away_mode(False)
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
if self.wink.current_fan_mode() == 'on':
|
||||
return STATE_ON
|
||||
elif self.wink.current_fan_mode() == 'auto':
|
||||
return STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
if self.wink.has_fan():
|
||||
return self.wink.fan_modes()
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Turn fan on/off."""
|
||||
self.wink.set_fan_mode(fan.lower())
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
self.set_operation_mode(STATE_AUX)
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
self.set_operation_mode(STATE_AUTO)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
minimum = 7 # Default minimum
|
||||
min_min = self.wink.min_min_set_point()
|
||||
min_max = self.wink.min_max_set_point()
|
||||
return_value = minimum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if min_min:
|
||||
return_value = min_min
|
||||
else:
|
||||
return_value = minimum
|
||||
elif self.current_operation == STATE_COOL:
|
||||
if min_max:
|
||||
return_value = min_max
|
||||
else:
|
||||
return_value = minimum
|
||||
elif self.current_operation == STATE_AUTO:
|
||||
if min_min and min_max:
|
||||
return_value = min(min_min, min_max)
|
||||
else:
|
||||
return_value = minimum
|
||||
else:
|
||||
return_value = minimum
|
||||
return return_value
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
maximum = 35 # Default maximum
|
||||
max_min = self.wink.max_min_set_point()
|
||||
max_max = self.wink.max_max_set_point()
|
||||
return_value = maximum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if max_min:
|
||||
return_value = max_min
|
||||
else:
|
||||
return_value = maximum
|
||||
elif self.current_operation == STATE_COOL:
|
||||
if max_max:
|
||||
return_value = max_max
|
||||
else:
|
||||
return_value = maximum
|
||||
elif self.current_operation == STATE_AUTO:
|
||||
if max_min and max_max:
|
||||
return_value = min(max_min, max_max)
|
||||
else:
|
||||
return_value = maximum
|
||||
else:
|
||||
return_value = maximum
|
||||
return return_value
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for ZWave climate devices.
|
||||
Support for Z-Wave climate devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.zwave/
|
||||
@@ -8,8 +8,7 @@ https://home-assistant.io/components/climate.zwave/
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ATTR_OPERATION_MODE)
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (
|
||||
@@ -18,44 +17,23 @@ from homeassistant.const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'ZWave Climate'
|
||||
DEFAULT_NAME = 'Z-Wave Climate'
|
||||
|
||||
REMOTEC = 0x5254
|
||||
REMOTEC_ZXT_120 = 0x8377
|
||||
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
|
||||
|
||||
HORSTMANN = 0x0059
|
||||
HORSTMANN_HRT4_ZW = 0x3
|
||||
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
|
||||
ATTR_OPERATING_STATE = 'operating_state'
|
||||
ATTR_FAN_STATE = 'fan_state'
|
||||
|
||||
WORKAROUND_ZXT_120 = 'zxt_120'
|
||||
WORKAROUND_HRT4_ZW = 'hrt4_zw'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120,
|
||||
HORSTMANN_HRT4_ZW_THERMOSTAT: WORKAROUND_HRT4_ZW
|
||||
}
|
||||
|
||||
SET_TEMP_TO_INDEX = {
|
||||
'Heat': 1,
|
||||
'Cool': 2,
|
||||
'Auto': 3,
|
||||
'Aux Heat': 4,
|
||||
'Resume': 5,
|
||||
'Fan Only': 6,
|
||||
'Furnace': 7,
|
||||
'Dry Air': 8,
|
||||
'Moist Air': 9,
|
||||
'Auto Changeover': 10,
|
||||
'Heat Econ': 11,
|
||||
'Cool Econ': 12,
|
||||
'Away': 13,
|
||||
'Unknown': 14
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the ZWave Climate devices."""
|
||||
"""Set up the Z-Wave Climate devices."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
@@ -69,30 +47,29 @@ 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."""
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, value, temp_unit):
|
||||
"""Initialize the zwave climate device."""
|
||||
"""Initialize the Z-Wave climate device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._index = value.index
|
||||
self._node = value.node
|
||||
self._target_temperature = None
|
||||
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._fan_state = None
|
||||
self._current_swing_mode = None
|
||||
self._swing_list = None
|
||||
self._unit = temp_unit
|
||||
self._index_operation = None
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
self._hrt4_zw = None
|
||||
self.update_properties()
|
||||
# register listener
|
||||
dispatcher.connect(
|
||||
@@ -107,17 +84,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||
" workaround")
|
||||
self._zxt_120 = 1
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_HRT4_ZW:
|
||||
_LOGGER.debug("Horstmann HRT4-ZW Zwave Thermostat"
|
||||
" workaround")
|
||||
self._hrt4_zw = 1
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
def update_properties(self):
|
||||
@@ -126,23 +99,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
self._current_operation = value.data
|
||||
self._index_operation = SET_TEMP_TO_INDEX.get(
|
||||
self._current_operation)
|
||||
self._operation_list = list(value.data_items)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s",
|
||||
self._current_operation)
|
||||
# Current Temp
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
if value.label == 'Temperature':
|
||||
self._current_temperature = int(value.data)
|
||||
self._current_temperature = round((float(value.data)), 1)
|
||||
self._unit = value.units
|
||||
# Fan Mode
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
self._current_fan_mode = value.data
|
||||
self._fan_list = list(value.data_items)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
@@ -150,9 +123,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
@@ -162,30 +136,39 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = int(self._current_temperature)
|
||||
break
|
||||
if self.current_operation is not None and \
|
||||
self.current_operation != 'Off':
|
||||
if self._index_operation != value.index:
|
||||
continue
|
||||
if self._zxt_120:
|
||||
temps = []
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
temps.append((round(float(value.data)), 1))
|
||||
if value.index == self._index:
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
break
|
||||
self._target_temperature = int(value.data)
|
||||
break
|
||||
_LOGGER.debug("Device can't set setpoint based on operation mode."
|
||||
" Defaulting to index=1")
|
||||
self._target_temperature = int(value.data)
|
||||
else:
|
||||
self._target_temperature = round((float(value.data)), 1)
|
||||
# Operating state
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const
|
||||
.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).values()):
|
||||
self._operating_state = value.data
|
||||
|
||||
# Fan operating state
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE)
|
||||
.values()):
|
||||
self._fan_state = value.data
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling on ZWave."""
|
||||
"""No polling on Z-Wave."""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -244,53 +227,19 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
else:
|
||||
return
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
_LOGGER.debug("set_temperature operation_mode=%s", operation_mode)
|
||||
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if operation_mode is not None:
|
||||
setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode)
|
||||
if value.index != setpoint_mode:
|
||||
continue
|
||||
_LOGGER.debug("setpoint_mode=%s", setpoint_mode)
|
||||
value.data = temperature
|
||||
break
|
||||
|
||||
if self.current_operation is not None:
|
||||
if self._hrt4_zw and self.current_operation == 'Off':
|
||||
# HRT4-ZW can change setpoint when off.
|
||||
value.data = int(temperature)
|
||||
if self._index_operation != value.index:
|
||||
continue
|
||||
_LOGGER.debug("self._index_operation=%s and"
|
||||
" self._current_operation=%s",
|
||||
self._index_operation,
|
||||
self._current_operation)
|
||||
if value.index == self._index:
|
||||
if self._zxt_120:
|
||||
_LOGGER.debug("zxt_120: Setting new setpoint for %s, "
|
||||
" operation=%s, temp=%s",
|
||||
self._index_operation,
|
||||
self._current_operation, temperature)
|
||||
# ZXT-120 does not support get setpoint
|
||||
self._target_temperature = temperature
|
||||
# ZXT-120 responds only to whole int
|
||||
value.data = round(temperature, 0)
|
||||
self._target_temperature = temperature
|
||||
self.update_ha_state()
|
||||
break
|
||||
else:
|
||||
_LOGGER.debug("Setting new setpoint for %s, "
|
||||
"operation=%s, temp=%s",
|
||||
self._index_operation,
|
||||
self._current_operation, temperature)
|
||||
value.data = temperature
|
||||
break
|
||||
else:
|
||||
_LOGGER.debug("Setting new setpoint for no known "
|
||||
"operation mode. Index=1 and "
|
||||
"temperature=%s", temperature)
|
||||
value.data = temperature
|
||||
self.update_ha_state()
|
||||
break
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
@@ -323,3 +272,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."""
|
||||
data = super().device_state_attributes
|
||||
if self._operating_state:
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state,
|
||||
if self._fan_state:
|
||||
data[ATTR_FAN_STATE] = self._fan_state
|
||||
return data
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.12.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.14.0']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.const import (
|
||||
@@ -67,7 +68,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."""
|
||||
|
||||
@@ -90,29 +90,30 @@ class MqttCover(CoverDevice):
|
||||
self._retain = retain
|
||||
self._optimistic = optimistic or state_topic is None
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._state_open:
|
||||
self._state = False
|
||||
_LOGGER.warning("state=%s", int(self._state))
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._state_closed:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload.isnumeric() and 0 <= int(payload) <= 100:
|
||||
if int(payload) > 0:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
self._position = int(payload)
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Payload is not True, False, or integer (0-100): %s",
|
||||
payload)
|
||||
|
||||
if self._state_topic is None:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
|
||||
@@ -18,7 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for covers."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
if not gateways:
|
||||
return
|
||||
|
||||
for gateway in gateways:
|
||||
pres = gateway.const.Presentation
|
||||
set_req = gateway.const.SetReq
|
||||
map_sv_types = {
|
||||
|
||||
@@ -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,14 +32,14 @@ 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:
|
||||
elif (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)])
|
||||
@@ -122,7 +122,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 +132,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)
|
||||
|
||||
@@ -17,6 +17,7 @@ DOMAIN = 'demo'
|
||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'alarm_control_panel',
|
||||
'binary_sensor',
|
||||
'calendar',
|
||||
'camera',
|
||||
'climate',
|
||||
'cover',
|
||||
|
||||
@@ -9,6 +9,7 @@ from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
@@ -41,7 +42,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__)
|
||||
@@ -80,21 +80,22 @@ def setup(hass, config):
|
||||
return None
|
||||
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
|
||||
|
||||
def turn_light_on_before_sunset(light_id):
|
||||
def async_turn_on_before_sunset(light_id):
|
||||
"""Helper function to turn on lights.
|
||||
|
||||
Speed is slow if there are devices home and the light is not on yet.
|
||||
"""
|
||||
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
|
||||
return
|
||||
light.turn_on(hass, light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
light.async_turn_on(hass, light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
|
||||
# Track every time sun rises so we can schedule a time-based
|
||||
# pre-sun set event
|
||||
@track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON,
|
||||
sun.STATE_ABOVE_HORIZON)
|
||||
@callback
|
||||
def schedule_lights_at_sun_set(hass, entity, old_state, new_state):
|
||||
"""The moment sun sets we want to have all the lights on.
|
||||
|
||||
@@ -105,16 +106,21 @@ def setup(hass, config):
|
||||
if not start_point:
|
||||
return
|
||||
|
||||
def turn_on(light_id):
|
||||
def async_turn_on_factory(light_id):
|
||||
"""Lambda can keep track of function parameters.
|
||||
|
||||
No local parameters. If we put the lambda directly in the below
|
||||
statement only the last light will be turned on.
|
||||
"""
|
||||
return lambda now: turn_light_on_before_sunset(light_id)
|
||||
@callback
|
||||
def async_turn_on_light(now):
|
||||
"""Turn on specific light."""
|
||||
async_turn_on_before_sunset(light_id)
|
||||
|
||||
return async_turn_on_light
|
||||
|
||||
for index, light_id in enumerate(light_ids):
|
||||
track_point_in_time(hass, turn_on(light_id),
|
||||
track_point_in_time(hass, async_turn_on_factory(light_id),
|
||||
start_point + index * LIGHT_TRANSITION_TIME)
|
||||
|
||||
# If the sun is already above horizon schedule the time-based pre-sun set
|
||||
@@ -123,6 +129,7 @@ def setup(hass, config):
|
||||
schedule_lights_at_sun_set(hass, None, None, None)
|
||||
|
||||
@track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME)
|
||||
@callback
|
||||
def check_light_on_dev_state_change(hass, entity, old_state, new_state):
|
||||
"""Handle tracked device state changes."""
|
||||
# pylint: disable=unused-variable
|
||||
@@ -137,7 +144,7 @@ def setup(hass, config):
|
||||
# Do we need lights?
|
||||
if light_needed:
|
||||
logger.info("Home coming event for %s. Turning lights on", entity)
|
||||
light.turn_on(hass, light_ids, profile=light_profile)
|
||||
light.async_turn_on(hass, light_ids, profile=light_profile)
|
||||
|
||||
# Are we in the time span were we would turn on the lights
|
||||
# if someone would be home?
|
||||
@@ -150,7 +157,7 @@ def setup(hass, config):
|
||||
# when the fading in started and turn it on if so
|
||||
for index, light_id in enumerate(light_ids):
|
||||
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
||||
light.turn_on(hass, light_id)
|
||||
light.async_turn_on(hass, light_id)
|
||||
|
||||
else:
|
||||
# If this light didn't happen to be turned on yet so
|
||||
@@ -159,6 +166,7 @@ def setup(hass, config):
|
||||
|
||||
if not disable_turn_off:
|
||||
@track_state_change(device_group, STATE_HOME, STATE_NOT_HOME)
|
||||
@callback
|
||||
def turn_off_lights_when_all_leave(hass, entity, old_state, new_state):
|
||||
"""Handle device group state change."""
|
||||
# pylint: disable=unused-variable
|
||||
@@ -167,6 +175,6 @@ def setup(hass, config):
|
||||
|
||||
logger.info(
|
||||
"Everyone has left but there are lights on. Turning them off")
|
||||
light.turn_off(hass, light_ids)
|
||||
light.async_turn_off(hass, light_ids)
|
||||
|
||||
return True
|
||||
|
||||
@@ -4,20 +4,17 @@ 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
|
||||
import threading
|
||||
from typing import Any, Sequence, Callable
|
||||
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
from homeassistant.bootstrap import (
|
||||
prepare_setup_platform, log_exception)
|
||||
async_prepare_setup_platform, async_log_exception)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
@@ -29,11 +26,12 @@ 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.helpers.event import async_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']
|
||||
@@ -57,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'
|
||||
@@ -88,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,
|
||||
@@ -103,19 +102,19 @@ 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)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Setup device tracker."""
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
|
||||
try:
|
||||
conf = config.get(DOMAIN, [])
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, DOMAIN, config)
|
||||
async_log_exception(ex, DOMAIN, config, hass)
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
@@ -123,60 +122,77 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
timedelta(seconds=DEFAULT_CONSIDER_HOME))
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
|
||||
devices = load_config(yaml_path, hass, consider_home)
|
||||
|
||||
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
||||
|
||||
def setup_platform(p_type, p_config, disc_info=None):
|
||||
# update tracked devices
|
||||
update_tasks = [device.async_update_ha_state() for device in devices
|
||||
if device.track]
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(p_type, p_config, disc_info=None):
|
||||
"""Setup a device tracker platform."""
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, p_type)
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if hasattr(platform, 'get_scanner'):
|
||||
scanner = platform.get_scanner(hass, {DOMAIN: p_config})
|
||||
scanner = yield from hass.loop.run_in_executor(
|
||||
None, platform.get_scanner, hass, {DOMAIN: p_config})
|
||||
|
||||
if scanner is None:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
setup_scanner_platform(hass, p_config, scanner, tracker.see)
|
||||
yield from async_setup_scanner_platform(
|
||||
hass, p_config, scanner, tracker.async_see)
|
||||
return
|
||||
|
||||
if not platform.setup_scanner(hass, p_config, tracker.see):
|
||||
ret = yield from hass.loop.run_in_executor(
|
||||
None, platform.setup_scanner, hass, p_config, tracker.see)
|
||||
if not ret:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error setting up platform %s', p_type)
|
||||
|
||||
for p_type, p_config in config_per_platform(config, DOMAIN):
|
||||
setup_platform(p_type, p_config)
|
||||
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
|
||||
in config_per_platform(config, DOMAIN)]
|
||||
if setup_tasks:
|
||||
yield from asyncio.wait(setup_tasks, loop=hass.loop)
|
||||
|
||||
def device_tracker_discovered(service, info):
|
||||
yield from tracker.async_setup_group()
|
||||
|
||||
@callback
|
||||
def async_device_tracker_discovered(service, info):
|
||||
"""Called when a device tracker platform is discovered."""
|
||||
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
|
||||
hass.async_add_job(
|
||||
async_setup_platform(DISCOVERY_PLATFORMS[service], {}, info))
|
||||
|
||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
|
||||
device_tracker_discovered)
|
||||
discovery.async_listen(
|
||||
hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered)
|
||||
|
||||
def update_stale(now):
|
||||
"""Clean up stale devices."""
|
||||
tracker.update_stale(now)
|
||||
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
|
||||
# Clean up stale devices
|
||||
async_track_utc_time_change(
|
||||
hass, tracker.async_update_stale, second=range(0, 60, 5))
|
||||
|
||||
tracker.setup_group()
|
||||
|
||||
def see_service(call):
|
||||
@asyncio.coroutine
|
||||
def async_see_service(call):
|
||||
"""Service to see a device."""
|
||||
args = {key: value for key, value in call.data.items() if key in
|
||||
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
|
||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
|
||||
tracker.see(**args)
|
||||
yield from tracker.async_see(**args)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_SEE, see_service,
|
||||
descriptions.get(SERVICE_SEE))
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
|
||||
|
||||
return True
|
||||
|
||||
@@ -190,88 +206,116 @@ class DeviceTracker(object):
|
||||
self.hass = hass
|
||||
self.devices = {dev.dev_id: dev for dev in devices}
|
||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new
|
||||
self.group = None # type: group.Group
|
||||
self._is_updating = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
for dev in devices:
|
||||
if self.devices[dev.dev_id] is not dev:
|
||||
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
|
||||
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
|
||||
_LOGGER.warning('Duplicate device MAC addresses detected %s',
|
||||
dev.mac)
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new
|
||||
self.lock = threading.Lock()
|
||||
|
||||
for device in devices:
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
|
||||
self.group = None # type: group.Group
|
||||
|
||||
def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
|
||||
battery: str=None, attributes: dict=None):
|
||||
"""Notify the device tracker that you see a device."""
|
||||
with self.lock:
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
elif mac is not None:
|
||||
mac = str(mac).upper()
|
||||
device = self.mac_to_dev.get(mac)
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = cv.slug(str(dev_id).lower())
|
||||
device = self.devices.get(dev_id)
|
||||
self.hass.add_job(
|
||||
self.async_see(mac, dev_id, host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
)
|
||||
|
||||
if device:
|
||||
device.seen(host_name, location_name, gps, gps_accuracy,
|
||||
battery, attributes)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
return
|
||||
@asyncio.coroutine
|
||||
def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||
location_name: str=None, gps: GPSType=None,
|
||||
gps_accuracy=None, battery: str=None, attributes: dict=None):
|
||||
"""Notify the device tracker that you see a device.
|
||||
|
||||
# If no device can be found, create it
|
||||
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
elif mac is not None:
|
||||
mac = str(mac).upper()
|
||||
device = self.mac_to_dev.get(mac)
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = cv.slug(str(dev_id).lower())
|
||||
device = self.devices.get(dev_id)
|
||||
|
||||
device.seen(host_name, location_name, gps, gps_accuracy, battery,
|
||||
attributes)
|
||||
if device:
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
yield from device.async_update_ha_state()
|
||||
return
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group is not None:
|
||||
self.group.update_tracked_entity_ids(
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
|
||||
# If no device can be found, create it
|
||||
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
def setup_group(self):
|
||||
"""Initialize group for all tracked devices."""
|
||||
run_coroutine_threadsafe(
|
||||
self.async_setup_group(), self.hass.loop).result()
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
|
||||
if device.track:
|
||||
yield from device.async_update_ha_state()
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
})
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group is not None:
|
||||
yield from self.group.async_update_tracked_entity_ids(
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
|
||||
# update known_devices.yaml
|
||||
self.hass.async_add_job(
|
||||
self.async_update_config(self.hass.config.path(YAML_DEVICES),
|
||||
dev_id, device)
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_config(self, path, dev_id, device):
|
||||
"""Add device to YAML configuration file.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
with (yield from self._is_updating):
|
||||
self.hass.loop.run_in_executor(
|
||||
None, update_config, self.hass.config.path(YAML_DEVICES),
|
||||
dev_id, device)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_group(self):
|
||||
"""Initialize group for all tracked devices.
|
||||
|
||||
This method must be run in the event loop.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||
if dev.track)
|
||||
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):
|
||||
"""Update stale devices."""
|
||||
with self.lock:
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home and
|
||||
device.stale(now)):
|
||||
device.update_ha_state(True)
|
||||
@callback
|
||||
def async_update_stale(self, now: dt_util.dt.datetime):
|
||||
"""Update stale devices.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home) and \
|
||||
device.stale(now):
|
||||
self.hass.async_add_job(device.async_update_ha_state(True))
|
||||
|
||||
|
||||
class Device(Entity):
|
||||
@@ -358,9 +402,10 @@ class Device(Entity):
|
||||
"""If device should be hidden."""
|
||||
return self.away_hide and self.state != STATE_HOME
|
||||
|
||||
def seen(self, host_name: str=None, location_name: str=None,
|
||||
gps: GPSType=None, gps_accuracy=0, battery: str=None,
|
||||
attributes: dict=None):
|
||||
@asyncio.coroutine
|
||||
def async_seen(self, host_name: str=None, location_name: str=None,
|
||||
gps: GPSType=None, gps_accuracy=0, battery: str=None,
|
||||
attributes: dict=None):
|
||||
"""Mark the device as seen."""
|
||||
self.last_seen = dt_util.utcnow()
|
||||
self.host_name = host_name
|
||||
@@ -369,28 +414,38 @@ class Device(Entity):
|
||||
self.battery = battery
|
||||
self.attributes = attributes
|
||||
self.gps = None
|
||||
|
||||
if gps is not None:
|
||||
try:
|
||||
self.gps = float(gps[0]), float(gps[1])
|
||||
except (ValueError, TypeError, IndexError):
|
||||
_LOGGER.warning('Could not parse gps value for %s: %s',
|
||||
self.dev_id, gps)
|
||||
self.update()
|
||||
|
||||
# pylint: disable=not-an-iterable
|
||||
yield from self.async_update()
|
||||
|
||||
def stale(self, now: dt_util.dt.datetime=None):
|
||||
"""Return if device state is stale."""
|
||||
"""Return if device state is stale.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return self.last_seen and \
|
||||
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
|
||||
|
||||
def update(self):
|
||||
"""Update state of entity."""
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update state of entity.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self.last_seen:
|
||||
return
|
||||
elif self.location_name:
|
||||
self._state = self.location_name
|
||||
elif self.gps is not None:
|
||||
zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1],
|
||||
self.gps_accuracy)
|
||||
zone_state = zone.async_active_zone(
|
||||
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
self._state = STATE_NOT_HOME
|
||||
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
||||
@@ -408,6 +463,17 @@ class Device(Entity):
|
||||
|
||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_load_config(path, hass, consider_home), hass.loop).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_load_config(path: str, hass: HomeAssistantType,
|
||||
consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
dev_schema = vol.Schema({
|
||||
vol.Required('name'): cv.string,
|
||||
vol.Optional('track', default=False): cv.boolean,
|
||||
@@ -422,7 +488,8 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
try:
|
||||
result = []
|
||||
try:
|
||||
devices = load_yaml_config_file(path)
|
||||
devices = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||
return []
|
||||
@@ -432,7 +499,7 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
device = dev_schema(device)
|
||||
device['dev_id'] = cv.slugify(dev_id)
|
||||
except vol.Invalid as exp:
|
||||
log_exception(exp, dev_id, devices)
|
||||
async_log_exception(exp, dev_id, devices, hass)
|
||||
else:
|
||||
result.append(Device(hass, **device))
|
||||
return result
|
||||
@@ -441,9 +508,13 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
return []
|
||||
|
||||
|
||||
def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
scanner: Any, see_device: Callable):
|
||||
"""Helper method to connect scanner-based platform to device tracker."""
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
scanner: Any, async_see_device: Callable):
|
||||
"""Helper method to connect scanner-based platform to device tracker.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
# Initial scan of each mac we also tell about host name for config
|
||||
@@ -451,25 +522,25 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
|
||||
def device_tracker_scan(now: dt_util.dt.datetime):
|
||||
"""Called when interval matches."""
|
||||
for mac in scanner.scan_devices():
|
||||
found_devices = scanner.scan_devices()
|
||||
|
||||
for mac in found_devices:
|
||||
if mac in seen:
|
||||
host_name = None
|
||||
else:
|
||||
host_name = scanner.get_device_name(mac)
|
||||
seen.add(mac)
|
||||
see_device(mac=mac, host_name=host_name)
|
||||
hass.async_add_job(async_see_device(mac=mac, host_name=host_name))
|
||||
|
||||
track_utc_time_change(hass, device_tracker_scan, second=range(0, 60,
|
||||
interval))
|
||||
async_track_utc_time_change(
|
||||
hass, device_tracker_scan, second=range(0, 60, interval))
|
||||
|
||||
device_tracker_scan(None)
|
||||
hass.async_add_job(device_tracker_scan, None)
|
||||
|
||||
|
||||
def update_config(path: str, dev_id: str, device: Device):
|
||||
"""Add device to YAML configuration file."""
|
||||
with open(path, 'a') as out:
|
||||
out.write('\n')
|
||||
|
||||
device = {device.dev_id: {
|
||||
'name': device.name,
|
||||
'mac': device.mac,
|
||||
@@ -477,11 +548,15 @@ def update_config(path: str, dev_id: str, device: Device):
|
||||
'track': device.track,
|
||||
CONF_AWAY_HIDE: device.away_hide
|
||||
}}
|
||||
yaml.dump(device, out, default_flow_style=False)
|
||||
out.write('\n')
|
||||
out.write(dump(device))
|
||||
|
||||
|
||||
def get_gravatar_for_email(email: str):
|
||||
"""Return an 80px Gravatar for the given email address."""
|
||||
"""Return an 80px Gravatar for the given email address.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
import hashlib
|
||||
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
|
||||
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())
|
||||
|
||||
@@ -42,6 +42,7 @@ def get_scanner(hass, config):
|
||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,15 @@ _IP_NEIGH_REGEX = re.compile(
|
||||
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' +
|
||||
r'(?P<status>(\w+))')
|
||||
|
||||
_NVRAM_CMD = 'nvram get client_info_tmp'
|
||||
_NVRAM_REGEX = re.compile(
|
||||
r'.*>.*>' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||
r'>' +
|
||||
r'(?P<mac>(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' +
|
||||
r'>' +
|
||||
r'.*')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
@@ -84,15 +93,14 @@ def get_scanner(hass, config):
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
|
||||
|
||||
|
||||
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]
|
||||
@@ -157,7 +165,8 @@ class AsusWrtDeviceScanner(object):
|
||||
active_clients = [client for client in data.values() if
|
||||
client['status'] == 'REACHABLE' or
|
||||
client['status'] == 'DELAY' or
|
||||
client['status'] == 'STALE']
|
||||
client['status'] == 'STALE' or
|
||||
client['status'] == 'IN_NVRAM']
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
@@ -186,13 +195,18 @@ class AsusWrtDeviceScanner(object):
|
||||
ssh.sendline(_WL_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.sendline(_NVRAM_CMD)
|
||||
ssh.prompt()
|
||||
nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:]
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
ssh.sendline(_LEASES_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.logout()
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except pxssh.ExceptionPxssh as exc:
|
||||
_LOGGER.error('Unexpected response from router: %s', exc)
|
||||
return None
|
||||
@@ -215,13 +229,18 @@ class AsusWrtDeviceScanner(object):
|
||||
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
|
||||
nvram_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1].split(b'<')[1:])
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except EOFError:
|
||||
_LOGGER.error('Unexpected response from router')
|
||||
return None
|
||||
@@ -279,6 +298,26 @@ class AsusWrtDeviceScanner(object):
|
||||
'ip': arp_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
# match mac addresses to IP addresses in NVRAM table
|
||||
for nvr in result.nvram:
|
||||
if match.group('mac').upper() in nvr.decode('utf-8'):
|
||||
nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8'))
|
||||
if not nvram_match:
|
||||
_LOGGER.warning('Could not parse nvr row: %s', nvr)
|
||||
continue
|
||||
|
||||
# skip current check if already in ARP table
|
||||
if nvram_match.group('ip') in devices.keys():
|
||||
continue
|
||||
|
||||
devices[nvram_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': 'IN_NVRAM',
|
||||
'ip': nvram_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
else:
|
||||
for lease in result.leases:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -7,15 +7,16 @@ 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
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
|
||||
REQUIREMENTS = ['pybbox==0.0.5-alpha']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pybbox==0.0.5-alpha']
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
@@ -36,7 +37,7 @@ class BboxDeviceScanner(object):
|
||||
self.last_results = [] # type: List[Device]
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info('Bbox scanner initialized')
|
||||
_LOGGER.info("Bbox scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
@@ -60,7 +61,7 @@ class BboxDeviceScanner(object):
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info('Scanning')
|
||||
_LOGGER.info("Scanning...")
|
||||
|
||||
import pybbox
|
||||
|
||||
@@ -78,5 +79,5 @@ class BboxDeviceScanner(object):
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info('Bbox scan successful')
|
||||
_LOGGER.info("Bbox scan successful")
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Support for Cisco IOS Routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.cisco_ios/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
|
||||
CONF_PORT
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=''): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Cisco scanner."""
|
||||
scanner = CiscoDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class CiscoDeviceScanner(object):
|
||||
"""This class queries a wireless router running Cisco IOS firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.port = config.get(CONF_PORT)
|
||||
self.password = config.get(CONF_PASSWORD)
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info('cisco_ios scanner initialized')
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""The firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return self.last_results
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensure the information from the Cisco router is up to date.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
string_result = self._get_arp_data()
|
||||
|
||||
if string_result:
|
||||
self.last_results = []
|
||||
last_results = []
|
||||
|
||||
lines_result = string_result.splitlines()
|
||||
|
||||
# Remove the first two lines, as they contains the arp command
|
||||
# and the arp table titles e.g.
|
||||
# show ip arp
|
||||
# Protocol Address | Age (min) | Hardware Addr | Type | Interface
|
||||
lines_result = lines_result[2:]
|
||||
|
||||
for line in lines_result:
|
||||
if len(line.split()) is 6:
|
||||
parts = line.split()
|
||||
if len(parts) != 6:
|
||||
continue
|
||||
|
||||
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
|
||||
# 'GigabitEthernet0']
|
||||
age = parts[2]
|
||||
hw_addr = parts[3]
|
||||
|
||||
if age != "-":
|
||||
mac = _parse_cisco_mac_address(hw_addr)
|
||||
age = int(age)
|
||||
if age < 1:
|
||||
last_results.append(mac)
|
||||
|
||||
self.last_results = last_results
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _get_arp_data(self):
|
||||
"""Open connection to the router and get arp entries."""
|
||||
from pexpect import pxssh
|
||||
import re
|
||||
|
||||
try:
|
||||
cisco_ssh = pxssh.pxssh()
|
||||
cisco_ssh.login(self.host, self.username, self.password,
|
||||
port=self.port, auto_prompt_reset=False)
|
||||
|
||||
# Find the hostname
|
||||
initial_line = cisco_ssh.before.decode('utf-8').splitlines()
|
||||
router_hostname = initial_line[len(initial_line) - 1]
|
||||
router_hostname += "#"
|
||||
# Set the discovered hostname as prompt
|
||||
regex_expression = ('(?i)^%s' % router_hostname).encode()
|
||||
cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE)
|
||||
# Allow full arp table to print at once
|
||||
cisco_ssh.sendline("terminal length 0")
|
||||
cisco_ssh.prompt(1)
|
||||
|
||||
cisco_ssh.sendline("show ip arp")
|
||||
cisco_ssh.prompt(1)
|
||||
|
||||
devices_result = cisco_ssh.before
|
||||
|
||||
return devices_result.decode("utf-8")
|
||||
except pxssh.ExceptionPxssh as px_e:
|
||||
_LOGGER.error("pxssh failed on login.")
|
||||
_LOGGER.error(px_e)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _parse_cisco_mac_address(cisco_hardware_addr):
|
||||
"""
|
||||
Parse a Cisco formatted HW address to normal MAC.
|
||||
|
||||
e.g. convert
|
||||
001d.ec02.07ab
|
||||
|
||||
to:
|
||||
00:1D:EC:02:07:AB
|
||||
|
||||
Takes in cisco_hwaddr: HWAddr String from Cisco ARP table
|
||||
Returns a regular standard MAC address
|
||||
"""
|
||||
cisco_hardware_addr = cisco_hardware_addr.replace('.', '')
|
||||
blocks = [cisco_hardware_addr[x:x + 2]
|
||||
for x in range(0, len(cisco_hardware_addr), 2)]
|
||||
|
||||
return ':'.join(blocks).upper()
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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,9 +4,13 @@ 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
|
||||
from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
STATE_NOT_HOME,
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.device_tracker import ( # NOQA
|
||||
@@ -19,7 +23,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 +39,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)
|
||||
@@ -66,9 +78,13 @@ class LocativeView(HomeAssistantView):
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||
|
||||
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,
|
||||
gps=gps_location))
|
||||
return 'Setting location to {}'.format(location_name)
|
||||
|
||||
elif direction == 'exit':
|
||||
@@ -76,7 +92,11 @@ 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)
|
||||
location_name = STATE_NOT_HOME
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
location_name=location_name,
|
||||
gps=gps_location))
|
||||
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.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Support for scanning a network with nmap.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.nmap_scanner/
|
||||
https://home-assistant.io/components/device_tracker.nmap_tracker/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
@@ -18,16 +18,16 @@ 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.ensure_list,
|
||||
@@ -43,6 +43,7 @@ def get_scanner(hass, config):
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
|
||||
|
||||
|
||||
@@ -73,7 +74,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 +98,7 @@ class NmapDeviceScanner(object):
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info('Scanning')
|
||||
_LOGGER.info("Scanning...")
|
||||
|
||||
from nmap import PortScanner, PortScannerError
|
||||
scanner = PortScanner()
|
||||
@@ -138,5 +139,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'
|
||||
|
||||
@@ -49,7 +49,6 @@ def get_scanner(hass, config):
|
||||
class SnmpScanner(object):
|
||||
"""Queries any SNMP capable Access Point for connected devices."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Support for Swisscom routers (Internet-Box).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.swisscom/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_IP = '192.168.1.1'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Return the Swisscom device scanner."""
|
||||
scanner = SwisscomDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SwisscomDeviceScanner(object):
|
||||
"""This class queries a router running Swisscom Internet-Box firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
# Test the router is accessible.
|
||||
data = self.get_swisscom_data()
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client['mac'] for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
if client['mac'] == device:
|
||||
return client['host']
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the Swisscom router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Loading data from Swisscom Internet Box")
|
||||
data = self.get_swisscom_data()
|
||||
if not data:
|
||||
return False
|
||||
|
||||
active_clients = [client for client in data.values() if
|
||||
client['status']]
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
def get_swisscom_data(self):
|
||||
"""Retrieve data from Swisscom and return parsed result."""
|
||||
url = 'http://{}/ws'.format(self.host)
|
||||
headers = {'Content-Type': 'application/x-sah-ws-4-call+json'}
|
||||
data = """
|
||||
{"service":"Devices", "method":"get",
|
||||
"parameters":{"expression":"lan and not self"}}"""
|
||||
|
||||
request = requests.post(url, headers=headers, data=data, timeout=10)
|
||||
|
||||
devices = {}
|
||||
for device in request.json()['status']:
|
||||
try:
|
||||
devices[device['Key']] = {
|
||||
'ip': device['IPAddress'],
|
||||
'mac': device['PhysAddress'],
|
||||
'host': device['Name'],
|
||||
'status': device['Active']
|
||||
}
|
||||
except (KeyError, requests.exceptions.RequestException):
|
||||
pass
|
||||
return devices
|
||||
@@ -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,9 +7,7 @@ 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
|
||||
@@ -27,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,
|
||||
@@ -40,62 +35,67 @@ 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])
|
||||
|
||||
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"]),
|
||||
fuel=vehicle["fuelAmount"],
|
||||
odometer=round(vehicle["odometer"] / 1000), # km
|
||||
range=vehicle["distanceToEmpty"])
|
||||
|
||||
if "heater" in vehicle and \
|
||||
"status" in vehicle["heater"]:
|
||||
attributes.update(heater_on=vehicle["heater"]["status"] != "off")
|
||||
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=(position["latitude"],
|
||||
position["longitude"]),
|
||||
attributes=attributes)
|
||||
|
||||
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_" + slugify(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())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user