mirror of
https://github.com/home-assistant/core.git
synced 2026-01-01 11:42:00 +01:00
Compare commits
272 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7461c57542 | ||
|
|
632f9a21b6 | ||
|
|
da44f80b32 | ||
|
|
8fb49e8687 | ||
|
|
5f8dc8af20 | ||
|
|
d267fc608f | ||
|
|
d3bc8519c0 | ||
|
|
d3adc6ddfb | ||
|
|
a3f586d097 | ||
|
|
f8c7fd212f | ||
|
|
b1f3492fd0 | ||
|
|
74acc5cf41 | ||
|
|
0bcb7839fb | ||
|
|
17237e9d3f | ||
|
|
a663dbada0 | ||
|
|
96e1d5524a | ||
|
|
33fd2250fd | ||
|
|
31f17a91e6 | ||
|
|
d0720ac699 | ||
|
|
05acf1c10a | ||
|
|
27c92937f2 | ||
|
|
a328df6014 | ||
|
|
1fb4eefc2c | ||
|
|
0f12b4c955 | ||
|
|
a9f14b67a8 | ||
|
|
445065700c | ||
|
|
4bd96fd437 | ||
|
|
5dde0c2201 | ||
|
|
6846a76c46 | ||
|
|
fa6e93f0c7 | ||
|
|
5ef274adce | ||
|
|
e39f7d3ef5 | ||
|
|
88b9503962 | ||
|
|
596093d564 | ||
|
|
23400c4b0a | ||
|
|
af54311718 | ||
|
|
442dcd584b | ||
|
|
1e4aec63ed | ||
|
|
80c187f8ea | ||
|
|
d73b695e73 | ||
|
|
f02d169864 | ||
|
|
2dd7f0616e | ||
|
|
2f2952e0ec | ||
|
|
8358542ce0 | ||
|
|
4ca5ed25bc | ||
|
|
7bf6ceafec | ||
|
|
1cfed4f015 | ||
|
|
a082ffca1d | ||
|
|
1b563b0640 | ||
|
|
1fe189e9cb | ||
|
|
edeb92ea42 | ||
|
|
c1095665e9 | ||
|
|
2a1f8af10a | ||
|
|
6234f2d73f | ||
|
|
b488663f2c | ||
|
|
a55d8776ff | ||
|
|
5ceb4c404d | ||
|
|
0061cece0c | ||
|
|
0099168ff8 | ||
|
|
87c89752ab | ||
|
|
45f6f4443a | ||
|
|
f1290d3135 | ||
|
|
746aae51ec | ||
|
|
da9430ed12 | ||
|
|
bef22076ea | ||
|
|
fe93b51017 | ||
|
|
07293e8d1e | ||
|
|
ca71d34076 | ||
|
|
548417761e | ||
|
|
7b8ad1d365 | ||
|
|
61cb6ec3dc | ||
|
|
349746f5f2 | ||
|
|
2e3b279873 | ||
|
|
f26861976d | ||
|
|
6bfeac7f80 | ||
|
|
a95fe588ca | ||
|
|
e5d11dd1a5 | ||
|
|
435e5c8a91 | ||
|
|
8d0553d9e6 | ||
|
|
9a239d1afb | ||
|
|
9252854f99 | ||
|
|
8d76e2679d | ||
|
|
4b1dcad7ae | ||
|
|
b45c386fd6 | ||
|
|
cb5fa79835 | ||
|
|
b74217bec2 | ||
|
|
fcf60e740d | ||
|
|
bb05600010 | ||
|
|
d3bb6d3988 | ||
|
|
f3945147a4 | ||
|
|
6398e92836 | ||
|
|
4d2b79156d | ||
|
|
b6d335f993 | ||
|
|
4b82c34b8f | ||
|
|
66fc852363 | ||
|
|
87274879a8 | ||
|
|
e4dbf8033c | ||
|
|
43db94d62d | ||
|
|
6d5fca2db1 | ||
|
|
d5e55448ef | ||
|
|
4ad998378f | ||
|
|
d46607c0d0 | ||
|
|
04920fa0bf | ||
|
|
1928da1fae | ||
|
|
06b051c53d | ||
|
|
473d765bb9 | ||
|
|
8e34c27b63 | ||
|
|
77aa2e940d | ||
|
|
3bbaf37193 | ||
|
|
b2d6ff9783 | ||
|
|
4fdde4f0e2 | ||
|
|
756768e745 | ||
|
|
83b791489b | ||
|
|
35132f9836 | ||
|
|
0e08785373 | ||
|
|
bf0dbdfd6a | ||
|
|
04407b8623 | ||
|
|
fb0ee34f10 | ||
|
|
ef63cfe8e4 | ||
|
|
e40f72e773 | ||
|
|
cec8ccb1a4 | ||
|
|
9b1ed4e79b | ||
|
|
8fffaebe50 | ||
|
|
84aab1c973 | ||
|
|
a2fbc0d2ef | ||
|
|
6a017efc0e | ||
|
|
363a429c41 | ||
|
|
9fc22ee47a | ||
|
|
a250f583eb | ||
|
|
bf495edbb5 | ||
|
|
3ea7dee83d | ||
|
|
d796e8db5c | ||
|
|
d24b45054a | ||
|
|
18935440ed | ||
|
|
2ba6b3a2ab | ||
|
|
2438c6b7c2 | ||
|
|
32a84f1466 | ||
|
|
0002a895ca | ||
|
|
d0b43b187a | ||
|
|
33d381731f | ||
|
|
18f81d7824 | ||
|
|
844c8149d7 | ||
|
|
7617864ba5 | ||
|
|
58c234466c | ||
|
|
9071946e87 | ||
|
|
b24aa24f6a | ||
|
|
1fde234c78 | ||
|
|
d67f3b8060 | ||
|
|
afb9cba806 | ||
|
|
1c2f4866e2 | ||
|
|
e90ae2fb75 | ||
|
|
4339e9aab1 | ||
|
|
9b640f6a81 | ||
|
|
437ddb8dea | ||
|
|
a119bd0056 | ||
|
|
0eaad46d93 | ||
|
|
8af6bacfd0 | ||
|
|
09ca440c20 | ||
|
|
74cc675a38 | ||
|
|
c478f2c7d0 | ||
|
|
a3a702b269 | ||
|
|
92a6f21cc2 | ||
|
|
814834512a | ||
|
|
46f3088a70 | ||
|
|
deed760008 | ||
|
|
d1da53615f | ||
|
|
69c919183a | ||
|
|
8eb29787a5 | ||
|
|
ae3973144c | ||
|
|
02f7eb9675 | ||
|
|
8c0967a190 | ||
|
|
bf2fe60cb5 | ||
|
|
1ddcab5e26 | ||
|
|
09fec29537 | ||
|
|
9189cbdc8b | ||
|
|
7fae8cd0f1 | ||
|
|
843f8ce9ee | ||
|
|
2bf781185f | ||
|
|
1e1d4c2013 | ||
|
|
bde711a9ff | ||
|
|
dc45ed38e7 | ||
|
|
03f916ed10 | ||
|
|
6e33c12008 | ||
|
|
401309c3b2 | ||
|
|
1c06b51968 | ||
|
|
e7de1fb9ae | ||
|
|
de0f6b781e | ||
|
|
314bce1073 | ||
|
|
9e16be3173 | ||
|
|
1b1619fbf1 | ||
|
|
1f226cffe9 | ||
|
|
b9ee5fb867 | ||
|
|
ba80d5e52a | ||
|
|
f2feabcf0b | ||
|
|
a19e7ba3f1 | ||
|
|
49d642741d | ||
|
|
db0efc647d | ||
|
|
640c692e1f | ||
|
|
4aef0b68bc | ||
|
|
c2b7c93375 | ||
|
|
8cc759ea4b | ||
|
|
a223efb840 | ||
|
|
c32807803e | ||
|
|
24a172163a | ||
|
|
372169a03a | ||
|
|
e4d100d54d | ||
|
|
bfd9623d8b | ||
|
|
3464454662 | ||
|
|
533bb5565b | ||
|
|
a8709a6988 | ||
|
|
4b767b088e | ||
|
|
c52b18d7c8 | ||
|
|
aaaf9637eb | ||
|
|
055db05946 | ||
|
|
0863d50210 | ||
|
|
1e352d37d0 | ||
|
|
620197b276 | ||
|
|
727a22f925 | ||
|
|
9bea7d7d8b | ||
|
|
97f62cfb78 | ||
|
|
482db94372 | ||
|
|
8a4e993183 | ||
|
|
790610525b | ||
|
|
7e668ef9e3 | ||
|
|
4dbf7be267 | ||
|
|
36eb0ceff3 | ||
|
|
d38acfbd39 | ||
|
|
b87e31617a | ||
|
|
bb6fe822f9 | ||
|
|
5504a511e3 | ||
|
|
5c96936eb4 | ||
|
|
cbbb15fa48 | ||
|
|
760138ac52 | ||
|
|
b1f538b622 | ||
|
|
ac8592587f | ||
|
|
aee25a020d | ||
|
|
13df925795 | ||
|
|
2b850f417e | ||
|
|
f303f6a191 | ||
|
|
f8cfa15152 | ||
|
|
12f731b32c | ||
|
|
11dcbd4449 | ||
|
|
fa6a089fb3 | ||
|
|
87da2ff1d7 | ||
|
|
b576df53e9 | ||
|
|
b90964faad | ||
|
|
549133a062 | ||
|
|
c29553517f | ||
|
|
2e27c0d5ec | ||
|
|
774f584ba8 | ||
|
|
81b1446aad | ||
|
|
6bfd52ada8 | ||
|
|
0646d01152 | ||
|
|
da5f5335eb | ||
|
|
c9d55cff23 | ||
|
|
aeb1d3d3fe | ||
|
|
a1c119adb6 | ||
|
|
84fe4f75df | ||
|
|
c07bf551d9 | ||
|
|
a745bf83ef | ||
|
|
1432ae649a | ||
|
|
cf1a27bd7c | ||
|
|
3d8b7a4122 | ||
|
|
e50588afe1 | ||
|
|
423e809e45 | ||
|
|
8461cf2717 | ||
|
|
9c9f5068b7 | ||
|
|
6d41024e76 | ||
|
|
7d24efc690 | ||
|
|
7d4adbbef5 | ||
|
|
e11ec88482 | ||
|
|
e39bdf8763 |
111
.coveragerc
111
.coveragerc
@@ -35,23 +35,35 @@ omit =
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
homeassistant/components/dweet.py
|
||||
homeassistant/components/*/dweet.py
|
||||
|
||||
homeassistant/components/eight_sleep.py
|
||||
homeassistant/components/*/eight_sleep.py
|
||||
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
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
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/*/hdmi_cec.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
homeassistant/components/*/insteon_local.py
|
||||
@@ -65,24 +77,48 @@ omit =
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/*/joaoapps_join.py
|
||||
|
||||
homeassistant/components/juicenet.py
|
||||
homeassistant/components/*/juicenet.py
|
||||
|
||||
homeassistant/components/kira.py
|
||||
homeassistant/components/*/kira.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
homeassistant/components/lutron_caseta.py
|
||||
homeassistant/components/*/lutron_caseta.py
|
||||
|
||||
homeassistant/components/mailgun.py
|
||||
homeassistant/components/*/mailgun.py
|
||||
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/mochad.py
|
||||
homeassistant/components/*/mochad.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
homeassistant/components/mysensors.py
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.py
|
||||
|
||||
homeassistant/components/nest.py
|
||||
homeassistant/components/*/nest.py
|
||||
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/octoprint.py
|
||||
homeassistant/components/*/octoprint.py
|
||||
|
||||
@@ -110,6 +146,9 @@ omit =
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/tellduslive.py
|
||||
homeassistant/components/*/tellduslive.py
|
||||
|
||||
@@ -142,45 +181,18 @@ omit =
|
||||
homeassistant/components/wink.py
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
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/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/mochad.py
|
||||
homeassistant/components/*/mochad.py
|
||||
|
||||
homeassistant/components/zabbix.py
|
||||
homeassistant/components/*/zabbix.py
|
||||
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/zha/__init__.py
|
||||
homeassistant/components/zha/const.py
|
||||
homeassistant/components/*/zha.py
|
||||
|
||||
homeassistant/components/eight_sleep.py
|
||||
homeassistant/components/*/eight_sleep.py
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
@@ -197,6 +209,7 @@ omit =
|
||||
homeassistant/components/binary_sensor/pilight.py
|
||||
homeassistant/components/binary_sensor/ping.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
@@ -204,8 +217,10 @@ omit =
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/flexit.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/knx.py
|
||||
@@ -215,11 +230,11 @@ omit =
|
||||
homeassistant/components/climate/sensibo.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/knx.py
|
||||
homeassistant/components/cover/myq.py
|
||||
homeassistant/components/cover/opengarage.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/cover/wink.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
@@ -233,6 +248,7 @@ omit =
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/linksys_ap.py
|
||||
homeassistant/components/device_tracker/linksys_smart.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/mikrotik.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
@@ -254,12 +270,10 @@ omit =
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/image_processing/dlib_face_detect.py
|
||||
homeassistant/components/image_processing/dlib_face_identify.py
|
||||
homeassistant/components/image_processing/seven_segments.py
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/light/avion.py
|
||||
@@ -269,7 +283,7 @@ omit =
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx/*.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
@@ -286,6 +300,7 @@ omit =
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/apple_tv.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
@@ -302,7 +317,6 @@ omit =
|
||||
homeassistant/components/media_player/frontier_silicon.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/hdmi_cec.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
@@ -310,6 +324,7 @@ omit =
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/nad.py
|
||||
homeassistant/components/media_player/nadtcp.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/openhome.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
@@ -331,17 +346,16 @@ omit =
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/mailgun.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
@@ -369,9 +383,13 @@ omit =
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
homeassistant/components/sensor/bh1750.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/blockchain.py
|
||||
homeassistant/components/sensor/bme280.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
@@ -391,6 +409,7 @@ omit =
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.py
|
||||
homeassistant/components/sensor/envirophat.py
|
||||
homeassistant/components/sensor/etherscan.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/fido.py
|
||||
@@ -398,6 +417,7 @@ omit =
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gitter.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
@@ -405,6 +425,7 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hddtemp.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/hydroquebec.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
@@ -435,6 +456,8 @@ omit =
|
||||
homeassistant/components/sensor/pushbullet.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
@@ -457,6 +480,7 @@ omit =
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/usps.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
@@ -464,6 +488,8 @@ omit =
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/shiftr.py
|
||||
homeassistant/components/spc.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
homeassistant/components/switch/arest.py
|
||||
@@ -472,7 +498,6 @@ omit =
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/fritzdect.py
|
||||
homeassistant/components/switch/hdmi_cec.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
homeassistant/components/switch/kankun.py
|
||||
@@ -492,8 +517,10 @@ omit =
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
homeassistant/components/weather/metoffice.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/yweather.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/util.py
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
.tox
|
||||
# General files
|
||||
.git
|
||||
.github
|
||||
config
|
||||
|
||||
# Test related files
|
||||
.tox
|
||||
|
||||
# Other virtualization methods
|
||||
venv
|
||||
.vagrant
|
||||
|
||||
# Temporary files
|
||||
**/__pycache__
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,3 +1,7 @@
|
||||
# Notice:
|
||||
# When updating this file, please also update virtualization/Docker/Dockerfile.dev
|
||||
# This way, the development image and the production image are kept in sync.
|
||||
|
||||
FROM python:3.6
|
||||
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
|
||||
@@ -8,6 +12,7 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_COAP_CLIENT no
|
||||
#ENV INSTALL_SSOCR no
|
||||
|
||||
VOLUME /config
|
||||
|
||||
@@ -20,8 +25,12 @@ RUN virtualization/Docker/setup_docker_prereqs
|
||||
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
|
||||
# Uninstall enum34 because some depenndecies install it but breaks Python 3.4+.
|
||||
# See PR #8103 for more info.
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet
|
||||
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet && \
|
||||
pip3 uninstall -y enum34
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
@@ -31,50 +31,8 @@ def attempt_use_uvloop():
|
||||
pass
|
||||
|
||||
|
||||
def monkey_patch_asyncio():
|
||||
"""Replace weakref.WeakSet to address Python 3 bug.
|
||||
|
||||
Under heavy threading operations that schedule calls into
|
||||
the asyncio event loop, Task objects are created. Due to
|
||||
a bug in Python, GC may have an issue when switching between
|
||||
the threads and objects with __del__ (which various components
|
||||
in HASS have).
|
||||
|
||||
This monkey-patch removes the weakref.Weakset, and replaces it
|
||||
with an object that ignores the only call utilizing it (the
|
||||
Task.__init__ which calls _all_tasks.add(self)). It also removes
|
||||
the __del__ which could trigger the future objects __del__ at
|
||||
unpredictable times.
|
||||
|
||||
The side-effect of this manipulation of the Task is that
|
||||
Task.all_tasks() is no longer accurate, and there will be no
|
||||
warning emitted if a Task is GC'd while in use.
|
||||
|
||||
On Python 3.6, after the bug is fixed, this monkey-patch can be
|
||||
disabled.
|
||||
|
||||
See https://bugs.python.org/issue26617 for details of the Python
|
||||
bug.
|
||||
"""
|
||||
# pylint: disable=no-self-use, protected-access, bare-except
|
||||
import asyncio.tasks
|
||||
|
||||
class IgnoreCalls:
|
||||
"""Ignore add calls."""
|
||||
|
||||
def add(self, other):
|
||||
"""No-op add."""
|
||||
return
|
||||
|
||||
asyncio.tasks.Task._all_tasks = IgnoreCalls()
|
||||
try:
|
||||
del asyncio.tasks.Task.__del__
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate we're running the right Python version."""
|
||||
"""Validate that the right Python version is running."""
|
||||
if sys.platform == "win32" and \
|
||||
sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
@@ -215,7 +173,7 @@ def daemonize() -> None:
|
||||
|
||||
|
||||
def check_pid(pid_file: str) -> None:
|
||||
"""Check that HA is not already running."""
|
||||
"""Check that Home Assistant is not already running."""
|
||||
# Check pid file
|
||||
try:
|
||||
pid = int(open(pid_file, 'r').readline())
|
||||
@@ -329,7 +287,7 @@ def setup_and_run_hass(config_dir: str,
|
||||
|
||||
|
||||
def try_to_restart() -> None:
|
||||
"""Attempt to clean up state and start a new homeassistant instance."""
|
||||
"""Attempt to clean up state and start a new Home Assistant instance."""
|
||||
# Things should be mostly shut down already at this point, now just try
|
||||
# to clean up things that may have been left behind.
|
||||
sys.stderr.write('Home Assistant attempting to restart.\n')
|
||||
@@ -361,11 +319,11 @@ def try_to_restart() -> None:
|
||||
else:
|
||||
os.closerange(3, max_fd)
|
||||
|
||||
# Now launch into a new instance of Home-Assistant. If this fails we
|
||||
# Now launch into a new instance of Home Assistant. If this fails we
|
||||
# fall through and exit with error 100 (RESTART_EXIT_CODE) in which case
|
||||
# systemd will restart us when RestartForceExitStatus=100 is set in the
|
||||
# systemd.service file.
|
||||
sys.stderr.write("Restarting Home-Assistant\n")
|
||||
sys.stderr.write("Restarting Home Assistant\n")
|
||||
args = cmdline()
|
||||
os.execv(args[0], args)
|
||||
|
||||
@@ -374,18 +332,13 @@ def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
if os.environ.get('HASS_MONKEYPATCH_ASYNCIO') == '1':
|
||||
if sys.version_info[:3] >= (3, 6):
|
||||
if os.environ.get('HASS_NO_MONKEY') != '1':
|
||||
if sys.version_info[:2] >= (3, 6):
|
||||
monkey_patch.disable_c_asyncio()
|
||||
monkey_patch.patch_weakref_tasks()
|
||||
elif sys.version_info[:3] < (3, 5, 3):
|
||||
monkey_patch.patch_weakref_tasks()
|
||||
|
||||
attempt_use_uvloop()
|
||||
|
||||
if sys.version_info[:3] < (3, 5, 3):
|
||||
monkey_patch_asyncio()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
if args.script is not None:
|
||||
|
||||
96
homeassistant/components/alarm_control_panel/spc.py
Normal file
96
homeassistant/components/alarm_control_panel/spc.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Support for Vanderbilt (formerly Siemens) SPC alarm systems.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.spc/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.spc import (
|
||||
SpcWebGateway, ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPC_AREA_MODE_TO_STATE = {'0': STATE_ALARM_DISARMED,
|
||||
'1': STATE_ALARM_ARMED_HOME,
|
||||
'3': STATE_ALARM_ARMED_AWAY}
|
||||
|
||||
|
||||
def _get_alarm_state(spc_mode):
|
||||
return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the SPC alarm control panel platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
||||
return
|
||||
|
||||
entities = [SpcAlarm(hass=hass,
|
||||
area_id=area['id'],
|
||||
name=area['name'],
|
||||
state=_get_alarm_state(area['mode']))
|
||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SpcAlarm(alarm.AlarmControlPanel):
|
||||
"""Represents the SPC alarm panel."""
|
||||
|
||||
def __init__(self, hass, area_id, name, state):
|
||||
"""Initialize the SPC alarm panel."""
|
||||
self._hass = hass
|
||||
self._area_id = area_id
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._api = hass.data[DATA_API]
|
||||
|
||||
hass.data[DATA_REGISTRY].register_alarm_device(area_id, self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state):
|
||||
"""Update the alarm panel with a new state."""
|
||||
self._state = state
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_SET)
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.verisure/
|
||||
"""
|
||||
import logging
|
||||
from time import sleep
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
@@ -20,20 +21,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Verisure platform."""
|
||||
alarms = []
|
||||
if int(hub.config.get(CONF_ALARM, 1)):
|
||||
hub.update_alarms()
|
||||
alarms.extend([
|
||||
VerisureAlarm(value.id)
|
||||
for value in hub.alarm_status.values()
|
||||
])
|
||||
hub.update_overview()
|
||||
alarms.append(VerisureAlarm())
|
||||
add_devices(alarms)
|
||||
|
||||
|
||||
def set_arm_state(state, code=None):
|
||||
"""Send set arm state command."""
|
||||
transaction_id = hub.session.set_arm_state(code, state)[
|
||||
'armStateChangeTransactionId']
|
||||
_LOGGER.info('verisure set arm state %s', state)
|
||||
transaction = {}
|
||||
while 'result' not in transaction:
|
||||
sleep(0.5)
|
||||
transaction = hub.session.get_arm_state_transaction(transaction_id)
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
hub.update_overview(no_throttle=True)
|
||||
|
||||
|
||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a Verisure alarm status."""
|
||||
|
||||
def __init__(self, device_id):
|
||||
"""Initialize the Verisure alarm panel."""
|
||||
self._id = device_id
|
||||
def __init__(self):
|
||||
"""Initalize the Verisure alarm panel."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = hub.config.get(CONF_CODE_DIGITS)
|
||||
self._changed_by = None
|
||||
@@ -41,18 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return 'Alarm {}'.format(self._id)
|
||||
return '{} alarm'.format(hub.session.installations[0]['alias'])
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return hub.available
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the code format as regex."""
|
||||
@@ -65,33 +70,26 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
hub.update_alarms()
|
||||
|
||||
if hub.alarm_status[self._id].status == 'unarmed':
|
||||
hub.update_overview()
|
||||
status = hub.get_first("$.armState.statusType")
|
||||
if status == 'DISARMED':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif hub.alarm_status[self._id].status == 'armedhome':
|
||||
elif status == 'ARMED_HOME':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif hub.alarm_status[self._id].status == 'armed':
|
||||
elif status == 'ARMED_AWAY':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif hub.alarm_status[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
"Unknown alarm state %s", hub.alarm_status[self._id].status)
|
||||
self._changed_by = hub.alarm_status[self._id].name
|
||||
elif status != 'PENDING':
|
||||
_LOGGER.error('Unknown alarm state %s', status)
|
||||
self._changed_by = hub.get_first("$.armState.name")
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
hub.my_pages.alarm.set(code, 'DISARMED')
|
||||
_LOGGER.info("Verisure alarm disarming")
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
set_arm_state('DISARMED', code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_HOME')
|
||||
_LOGGER.info("Verisure alarm arming home")
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
set_arm_state('ARMED_HOME', code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
|
||||
_LOGGER.info("Verisure alarm arming away")
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
set_arm_state('ARMED_AWAY', code)
|
||||
|
||||
@@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'alert'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
CONF_DONE_MESSAGE = 'done_message'
|
||||
CONF_CAN_ACK = 'can_acknowledge'
|
||||
CONF_NOTIFIERS = 'notifiers'
|
||||
CONF_REPEAT = 'repeat'
|
||||
@@ -35,6 +36,7 @@ DEFAULT_SKIP_FIRST = False
|
||||
|
||||
ALERT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DONE_MESSAGE, default=None): cv.string,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
||||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||
@@ -121,10 +123,10 @@ def async_setup(hass, config):
|
||||
# Setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
entity = Alert(hass, entity_id,
|
||||
alert[CONF_NAME], alert[CONF_ENTITY_ID],
|
||||
alert[CONF_STATE], alert[CONF_REPEAT],
|
||||
alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS],
|
||||
alert[CONF_CAN_ACK])
|
||||
alert[CONF_NAME], alert[CONF_DONE_MESSAGE],
|
||||
alert[CONF_ENTITY_ID], alert[CONF_STATE],
|
||||
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
|
||||
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
|
||||
all_alerts[entity.entity_id] = entity
|
||||
|
||||
# Read descriptions
|
||||
@@ -154,8 +156,8 @@ def async_setup(hass, config):
|
||||
class Alert(ToggleEntity):
|
||||
"""Representation of an alert."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, watched_entity_id, state,
|
||||
repeat, skip_first, notifiers, can_ack):
|
||||
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
|
||||
state, repeat, skip_first, notifiers, can_ack):
|
||||
"""Initialize the alert."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@@ -163,6 +165,7 @@ class Alert(ToggleEntity):
|
||||
self._skip_first = skip_first
|
||||
self._notifiers = notifiers
|
||||
self._can_ack = can_ack
|
||||
self._done_message = done_message
|
||||
|
||||
self._delay = [timedelta(minutes=val) for val in repeat]
|
||||
self._next_delay = 0
|
||||
@@ -170,6 +173,7 @@ class Alert(ToggleEntity):
|
||||
self._firing = False
|
||||
self._ack = False
|
||||
self._cancel = None
|
||||
self._send_done_message = False
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
|
||||
|
||||
event.async_track_state_change(
|
||||
@@ -230,6 +234,8 @@ class Alert(ToggleEntity):
|
||||
self._cancel()
|
||||
self._ack = False
|
||||
self._firing = False
|
||||
if self._done_message and self._send_done_message:
|
||||
yield from self._notify_done_message()
|
||||
self.hass.async_add_job(self.async_update_ha_state)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -249,11 +255,21 @@ class Alert(ToggleEntity):
|
||||
|
||||
if not self._ack:
|
||||
_LOGGER.info("Alerting: %s", self._name)
|
||||
self._send_done_message = True
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._name})
|
||||
yield from self._schedule_notify()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _notify_done_message(self, *args):
|
||||
"""Send notification of complete alert."""
|
||||
_LOGGER.info("Alerting: %s", self._done_message)
|
||||
self._send_done_message = False
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._done_message})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self):
|
||||
"""Async Unacknowledge alert."""
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
"""
|
||||
This component provides basic support for Netgear Arlo IP cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/arlo/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.loader as loader
|
||||
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com'
|
||||
|
||||
DOMAIN = 'arlo'
|
||||
CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
|
||||
|
||||
DATA_ARLO = 'data_arlo'
|
||||
DEFAULT_BRAND = 'Netgear Arlo'
|
||||
DOMAIN = 'arlo'
|
||||
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Camera Setup'
|
||||
@@ -47,7 +47,7 @@ def setup(hass, config):
|
||||
arlo = PyArlo(username, password, preload=False)
|
||||
if not arlo.is_connected:
|
||||
return False
|
||||
hass.data['arlo'] = arlo
|
||||
hass.data[DATA_ARLO] = arlo
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
|
||||
@@ -29,6 +29,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
|
||||
DOMAIN = 'automation'
|
||||
DEPENDENCIES = ['group']
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||
|
||||
@@ -11,6 +11,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
CONF_HOST, CONF_INCLUDE, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_TRIGGER_TIME,
|
||||
@@ -18,11 +19,12 @@ from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
|
||||
REQUIREMENTS = ['axis==7']
|
||||
REQUIREMENTS = ['axis==8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -59,6 +61,21 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_VAPIX_CALL = 'vapix_call'
|
||||
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response'
|
||||
SERVICE_CGI = 'cgi'
|
||||
SERVICE_ACTION = 'action'
|
||||
SERVICE_PARAM = 'param'
|
||||
SERVICE_DEFAULT_CGI = 'param.cgi'
|
||||
SERVICE_DEFAULT_ACTION = 'update'
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(SERVICE_PARAM): cv.string,
|
||||
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
|
||||
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def request_configuration(hass, name, host, serialnumber):
|
||||
"""Request configuration steps from the user."""
|
||||
@@ -135,23 +152,34 @@ def setup(hass, base_config):
|
||||
|
||||
def axis_device_discovered(service, discovery_info):
|
||||
"""Called when axis devices has been found."""
|
||||
host = discovery_info['host']
|
||||
host = discovery_info[CONF_HOST]
|
||||
name = discovery_info['hostname']
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
|
||||
if serialnumber not in AXIS_DEVICES:
|
||||
config_file = _read_config(hass)
|
||||
if serialnumber in config_file:
|
||||
# Device config saved to file
|
||||
try:
|
||||
config = DEVICE_SCHEMA(config_file[serialnumber])
|
||||
config[CONF_HOST] = host
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
||||
return False
|
||||
if not setup_device(hass, config):
|
||||
_LOGGER.error("Couldn\'t set up %s", config['name'])
|
||||
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
|
||||
else:
|
||||
# New device, create configuration request for UI
|
||||
request_configuration(hass, name, host, serialnumber)
|
||||
else:
|
||||
# Device already registered, but on a different IP
|
||||
device = AXIS_DEVICES[serialnumber]
|
||||
device.url = host
|
||||
async_dispatcher_send(hass,
|
||||
DOMAIN + '_' + device.name + '_new_ip',
|
||||
host)
|
||||
|
||||
# Register discovery service
|
||||
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
|
||||
|
||||
if DOMAIN in base_config:
|
||||
@@ -160,7 +188,30 @@ def setup(hass, base_config):
|
||||
if CONF_NAME not in config:
|
||||
config[CONF_NAME] = device
|
||||
if not setup_device(hass, config):
|
||||
_LOGGER.error("Couldn\'t set up %s", config['name'])
|
||||
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
|
||||
|
||||
# Services to communicate with device.
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def vapix_service(call):
|
||||
"""Service to send a message."""
|
||||
for _, device in AXIS_DEVICES.items():
|
||||
if device.name == call.data[CONF_NAME]:
|
||||
response = device.do_request(call.data[SERVICE_CGI],
|
||||
call.data[SERVICE_ACTION],
|
||||
call.data[SERVICE_PARAM])
|
||||
hass.bus.async_fire(SERVICE_VAPIX_CALL_RESPONSE, response)
|
||||
return True
|
||||
_LOGGER.info("Couldn\'t find device %s", call.data[CONF_NAME])
|
||||
return False
|
||||
|
||||
# Register service with Home Assistant.
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_VAPIX_CALL,
|
||||
vapix_service,
|
||||
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -190,8 +241,16 @@ def setup_device(hass, config):
|
||||
|
||||
if enable_metadatastream:
|
||||
device.initialize_new_event = event_initialized
|
||||
device.initiate_metadatastream()
|
||||
if not device.initiate_metadatastream():
|
||||
notification = get_component('persistent_notification')
|
||||
notification.create(hass,
|
||||
'Dependency missing for sensors, '
|
||||
'please check documentation',
|
||||
title=DOMAIN,
|
||||
notification_id='axis_notification')
|
||||
|
||||
AXIS_DEVICES[device.serial_number] = device
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -311,4 +370,4 @@ REMAP = [{'type': 'motion',
|
||||
'class': 'input',
|
||||
'topic': 'tns1:Device/tnsaxis:IO/Port',
|
||||
'subscribe': 'onvif:Device/axis:IO/Port',
|
||||
'platform': 'sensor'}, ]
|
||||
'platform': 'binary_sensor'}, ]
|
||||
|
||||
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices([ArestBinarySensor(
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
device_class, pin)])
|
||||
device_class, pin)], True)
|
||||
|
||||
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
@@ -64,7 +64,6 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
self._name = name
|
||||
self._device_class = device_class
|
||||
self._pin = pin
|
||||
self.update()
|
||||
|
||||
if self._pin is not None:
|
||||
request = requests.get(
|
||||
|
||||
@@ -8,19 +8,18 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.digital_ocean import (
|
||||
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
||||
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||
ATTR_REGION, ATTR_VCPUS)
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Droplet'
|
||||
DEFAULT_SENSOR_CLASS = 'motion'
|
||||
DEFAULT_SENSOR_CLASS = 'moving'
|
||||
DEPENDENCIES = ['digital_ocean']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -30,19 +29,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Digital Ocean droplet sensor."""
|
||||
digital_ocean = get_component('digital_ocean')
|
||||
digital = hass.data.get(DATA_DIGITAL_OCEAN)
|
||||
if not digital:
|
||||
return False
|
||||
|
||||
droplets = config.get(CONF_DROPLETS)
|
||||
|
||||
dev = []
|
||||
for droplet in droplets:
|
||||
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
|
||||
droplet_id = digital.get_droplet_id(droplet)
|
||||
if droplet_id is None:
|
||||
_LOGGER.error("Droplet %s is not available", droplet)
|
||||
return False
|
||||
dev.append(DigitalOceanBinarySensor(
|
||||
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||
dev.append(DigitalOceanBinarySensor(digital, droplet_id))
|
||||
|
||||
add_devices(dev)
|
||||
add_devices(dev, True)
|
||||
|
||||
|
||||
class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
@@ -53,7 +54,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
self._digital_ocean = do
|
||||
self._droplet_id = droplet_id
|
||||
self._state = None
|
||||
self.update()
|
||||
self.data = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -80,6 +80,12 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
elif value2 == 0x10:
|
||||
self.which = 1
|
||||
self.onoff = 1
|
||||
elif value2 == 0x37:
|
||||
self.which = 10
|
||||
self.onoff = 0
|
||||
elif value2 == 0x15:
|
||||
self.which = 10
|
||||
self.onoff = 1
|
||||
self.hass.bus.fire('button_pressed', {'id': self.dev_id,
|
||||
'pushed': value,
|
||||
'which': self.which,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for Homematic binary sensors.
|
||||
Support for HomeMatic binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematic/
|
||||
@@ -29,7 +29,7 @@ SENSOR_TYPES_CLASS = {
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Homematic binary sensor platform."""
|
||||
"""Set up the HomeMatic binary sensor platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
@@ -43,7 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
"""Representation of a binary Homematic device."""
|
||||
"""Representation of a binary HomeMatic device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -54,16 +54,14 @@ class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
# If state is MOTION (RemoteMotion works only)
|
||||
"""Return the class of this sensor from DEVICE_CLASSES."""
|
||||
# If state is MOTION (Only RemoteMotion working)
|
||||
if self._state == 'MOTION':
|
||||
return 'motion'
|
||||
return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None)
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data struct (self._data) from the Homematic metadata."""
|
||||
# add state to data struct
|
||||
"""Generate the data dictionary (self._data) from metadata."""
|
||||
# Add state to data struct
|
||||
if self._state:
|
||||
_LOGGER.debug("%s init datastruct with main node '%s'", self._name,
|
||||
self._state)
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
|
||||
@@ -50,6 +50,10 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
self._coil = int(coil)
|
||||
self._value = None
|
||||
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
233
homeassistant/components/binary_sensor/rfxtrx.py
Normal file
233
homeassistant/components/binary_sensor/rfxtrx.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
Support for RFXtrx binary sensors.
|
||||
|
||||
Lighting4 devices (sensors based on PT2262 encoder) are supported and
|
||||
tested. Other types may need some work.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.components import rfxtrx
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import event as evt
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_OFF_DELAY, ATTR_FIREEVENT,
|
||||
ATTR_DATA_BITS, CONF_DEVICES
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["rfxtrx"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required("platform"): rfxtrx.DOMAIN,
|
||||
vol.Optional(CONF_DEVICES, default={}): vol.All(
|
||||
dict, rfxtrx.valid_binary_sensor),
|
||||
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the Binary Sensor platform to rfxtrx."""
|
||||
import RFXtrx as rfxtrxmod
|
||||
sensors = []
|
||||
|
||||
for packet_id, entity in config['devices'].items():
|
||||
event = rfxtrx.get_rfx_object(packet_id)
|
||||
device_id = slugify(event.device.id_string.lower())
|
||||
|
||||
if device_id in rfxtrx.RFX_DEVICES:
|
||||
continue
|
||||
|
||||
if entity[ATTR_DATA_BITS] is not None:
|
||||
_LOGGER.info("Masked device id: %s",
|
||||
rfxtrx.get_pt2262_deviceid(device_id,
|
||||
entity[ATTR_DATA_BITS]))
|
||||
|
||||
_LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)",
|
||||
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
|
||||
|
||||
device = RfxtrxBinarySensor(event, entity[ATTR_NAME],
|
||||
entity[CONF_DEVICE_CLASS],
|
||||
entity[ATTR_FIREEVENT],
|
||||
entity[ATTR_OFF_DELAY],
|
||||
entity[ATTR_DATA_BITS],
|
||||
entity[CONF_COMMAND_ON],
|
||||
entity[CONF_COMMAND_OFF])
|
||||
device.hass = hass
|
||||
sensors.append(device)
|
||||
rfxtrx.RFX_DEVICES[device_id] = device
|
||||
|
||||
add_devices_callback(sensors)
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def binary_sensor_update(event):
|
||||
"""Callback for control updates from the RFXtrx gateway."""
|
||||
if not isinstance(event, rfxtrxmod.ControlEvent):
|
||||
return
|
||||
|
||||
device_id = slugify(event.device.id_string.lower())
|
||||
|
||||
if device_id in rfxtrx.RFX_DEVICES:
|
||||
sensor = rfxtrx.RFX_DEVICES[device_id]
|
||||
else:
|
||||
sensor = rfxtrx.get_pt2262_device(device_id)
|
||||
|
||||
if sensor is None:
|
||||
# Add the entity if not exists and automatic_add is True
|
||||
if not config[ATTR_AUTOMATIC_ADD]:
|
||||
return
|
||||
|
||||
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
|
||||
|
||||
if poss_dev is not None:
|
||||
poss_id = slugify(poss_dev.event.device.id_string.lower())
|
||||
_LOGGER.info("Found possible matching deviceid %s.",
|
||||
poss_id)
|
||||
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
sensor = RfxtrxBinarySensor(event, pkt_id)
|
||||
rfxtrx.RFX_DEVICES[device_id] = sensor
|
||||
add_devices_callback([sensor])
|
||||
_LOGGER.info("Added binary sensor %s "
|
||||
"(Device_id: %s Class: %s Sub: %s)",
|
||||
pkt_id,
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype)
|
||||
|
||||
elif not isinstance(sensor, RfxtrxBinarySensor):
|
||||
return
|
||||
else:
|
||||
_LOGGER.info("Binary sensor update "
|
||||
"(Device_id: %s Class: %s Sub: %s)",
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype)
|
||||
|
||||
if sensor.is_pt2262:
|
||||
cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits)
|
||||
_LOGGER.info("applying cmd %s to device_id: %s)",
|
||||
cmd, sensor.masked_id)
|
||||
sensor.apply_cmd(int(cmd, 16))
|
||||
else:
|
||||
rfxtrx.apply_received_command(event)
|
||||
|
||||
if (sensor.is_on and sensor.off_delay is not None and
|
||||
sensor.delay_listener is None):
|
||||
|
||||
def off_delay_listener(now):
|
||||
"""Switch device off after a delay."""
|
||||
sensor.delay_listener = None
|
||||
sensor.update_state(False)
|
||||
|
||||
sensor.delay_listener = evt.track_point_in_time(
|
||||
hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay
|
||||
)
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes,too-many-arguments
|
||||
class RfxtrxBinarySensor(BinarySensorDevice):
|
||||
"""An Rfxtrx binary sensor."""
|
||||
|
||||
def __init__(self, event, name, device_class=None,
|
||||
should_fire=False, off_delay=None, data_bits=None,
|
||||
cmd_on=None, cmd_off=None):
|
||||
"""Initialize the sensor."""
|
||||
self.event = event
|
||||
self._name = name
|
||||
self._should_fire_event = should_fire
|
||||
self._device_class = device_class
|
||||
self._off_delay = off_delay
|
||||
self._state = False
|
||||
self.delay_listener = None
|
||||
self._data_bits = data_bits
|
||||
self._cmd_on = cmd_on
|
||||
self._cmd_off = cmd_off
|
||||
|
||||
if data_bits is not None:
|
||||
self._masked_id = rfxtrx.get_pt2262_deviceid(
|
||||
event.device.id_string.lower(),
|
||||
data_bits)
|
||||
|
||||
def __str__(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_pt2262(self):
|
||||
"""Return true if the device is PT2262-based."""
|
||||
return self._data_bits is not None
|
||||
|
||||
@property
|
||||
def masked_id(self):
|
||||
"""Return the masked device id (isolated address bits)."""
|
||||
return self._masked_id
|
||||
|
||||
@property
|
||||
def data_bits(self):
|
||||
"""Return the number of data bits."""
|
||||
return self._data_bits
|
||||
|
||||
@property
|
||||
def cmd_on(self):
|
||||
"""Return the value of the 'On' command."""
|
||||
return self._cmd_on
|
||||
|
||||
@property
|
||||
def cmd_off(self):
|
||||
"""Return the value of the 'Off' command."""
|
||||
return self._cmd_off
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def should_fire_event(self):
|
||||
"""Return is the device must fire event."""
|
||||
return self._should_fire_event
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the sensor class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def off_delay(self):
|
||||
"""Return the off_delay attribute value."""
|
||||
return self._off_delay
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the sensor state is True."""
|
||||
return self._state
|
||||
|
||||
def apply_cmd(self, cmd):
|
||||
"""Apply a command for updating the state."""
|
||||
if cmd == self.cmd_on:
|
||||
self.update_state(True)
|
||||
elif cmd == self.cmd_off:
|
||||
self.update_state(False)
|
||||
|
||||
def update_state(self, state):
|
||||
"""Update the state of the device."""
|
||||
self._state = state
|
||||
self.schedule_update_ha_state()
|
||||
99
homeassistant/components/binary_sensor/spc.py
Normal file
99
homeassistant/components/binary_sensor/spc.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Support for Vanderbilt (formerly Siemens) SPC alarm systems.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.spc/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.spc import (
|
||||
ATTR_DISCOVER_DEVICES, DATA_REGISTRY)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import (STATE_UNAVAILABLE, STATE_ON, STATE_OFF)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPC_TYPE_TO_DEVICE_CLASS = {'0': 'motion',
|
||||
'1': 'opening',
|
||||
'3': 'smoke'}
|
||||
|
||||
|
||||
SPC_INPUT_TO_SENSOR_STATE = {'0': STATE_OFF,
|
||||
'1': STATE_ON}
|
||||
|
||||
|
||||
def _get_device_class(spc_type):
|
||||
return SPC_TYPE_TO_DEVICE_CLASS.get(spc_type, None)
|
||||
|
||||
|
||||
def _get_sensor_state(spc_input):
|
||||
return SPC_INPUT_TO_SENSOR_STATE.get(spc_input, STATE_UNAVAILABLE)
|
||||
|
||||
|
||||
def _create_sensor(hass, zone):
|
||||
return SpcBinarySensor(zone_id=zone['id'],
|
||||
name=zone['zone_name'],
|
||||
state=_get_sensor_state(zone['input']),
|
||||
device_class=_get_device_class(zone['type']),
|
||||
spc_registry=hass.data[DATA_REGISTRY])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Initialize the platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
_create_sensor(hass, zone)
|
||||
for zone in discovery_info[ATTR_DISCOVER_DEVICES]
|
||||
if _get_device_class(zone['type']))
|
||||
|
||||
|
||||
class SpcBinarySensor(BinarySensorDevice):
|
||||
"""Represents a sensor based on an SPC zone."""
|
||||
|
||||
def __init__(self, zone_id, name, state, device_class, spc_registry):
|
||||
"""Initialize the sensor device."""
|
||||
self._zone_id = zone_id
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._device_class = device_class
|
||||
|
||||
spc_registry.register_sensor_device(zone_id, self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state):
|
||||
"""Update the state of the device."""
|
||||
self._state = state
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Whether the device is switched on."""
|
||||
return self._state == STATE_ON
|
||||
|
||||
@property
|
||||
def hidden(self) -> bool:
|
||||
"""Whether the device is hidden by default."""
|
||||
# these type of sensors are probably mainly used for automations
|
||||
return True
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""The device class."""
|
||||
return self._device_class
|
||||
86
homeassistant/components/binary_sensor/tapsaff.py
Normal file
86
homeassistant/components/binary_sensor/tapsaff.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Support for Taps Affs.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.tapsaff/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['tapsaff==0.1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_LOCATION = 'location'
|
||||
|
||||
DEFAULT_NAME = 'Taps Aff'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=30)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_LOCATION): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Taps Aff binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
location = config.get(CONF_LOCATION)
|
||||
|
||||
taps_aff_data = TapsAffData(location)
|
||||
|
||||
add_devices([TapsAffSensor(taps_aff_data, name)], True)
|
||||
|
||||
|
||||
class TapsAffSensor(BinarySensorDevice):
|
||||
"""Implementation of a Taps Aff binary sensor."""
|
||||
|
||||
def __init__(self, taps_aff_data, name):
|
||||
"""Initialize the Taps Aff sensor."""
|
||||
self.data = taps_aff_data
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return '{}'.format(self._name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if taps aff."""
|
||||
return self.data.is_taps_aff
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
self.data.update()
|
||||
|
||||
|
||||
class TapsAffData(object):
|
||||
"""Class for handling the data retrieval for pins."""
|
||||
|
||||
def __init__(self, location):
|
||||
"""Initialize the sensor."""
|
||||
from tapsaff import TapsAff
|
||||
|
||||
self._is_taps_aff = None
|
||||
self.taps_aff = TapsAff(location)
|
||||
|
||||
@property
|
||||
def is_taps_aff(self):
|
||||
"""Return true if taps aff."""
|
||||
return self._is_taps_aff
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from the Taps Aff API and updates the states."""
|
||||
try:
|
||||
self._is_taps_aff = self.taps_aff.is_taps_aff
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Update failed. Check configured location")
|
||||
59
homeassistant/components/binary_sensor/verisure.py
Normal file
59
homeassistant/components/binary_sensor/verisure.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Interfaces with Verisure sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.verisure/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.verisure import CONF_DOOR_WINDOW
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Verisure binary sensors."""
|
||||
sensors = []
|
||||
hub.update_overview()
|
||||
|
||||
if int(hub.config.get(CONF_DOOR_WINDOW, 1)):
|
||||
sensors.extend([
|
||||
VerisureDoorWindowSensor(device_label)
|
||||
for device_label in hub.get(
|
||||
"$.doorWindow.doorWindowDevice[*].deviceLabel")])
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
class VerisureDoorWindowSensor(BinarySensorDevice):
|
||||
"""Verisure door window sensor."""
|
||||
|
||||
def __init__(self, device_label):
|
||||
"""Initialize the modbus coil sensor."""
|
||||
self._device_label = device_label
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return hub.get_first(
|
||||
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area",
|
||||
self._device_label)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
return hub.get_first(
|
||||
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state",
|
||||
self._device_label) == "OPEN"
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return hub.get_first(
|
||||
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
|
||||
self._device_label) is not None
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the sensor."""
|
||||
hub.update_overview()
|
||||
@@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
if disc_info is None:
|
||||
return
|
||||
|
||||
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
||||
if not any(data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]):
|
||||
return
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
@@ -39,7 +39,6 @@ def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
"""A calendar event device."""
|
||||
|
||||
|
||||
@@ -12,13 +12,16 @@ from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
from random import SystemRandom
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import ATTR_ENTITY_PICTURE
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -26,9 +29,12 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_EN_MOTION = 'enable_motion_detection'
|
||||
SERVICE_DISEN_MOTION = 'disable_motion_detection'
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@@ -38,11 +44,30 @@ STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
|
||||
DEFAULT_CONTENT_TYPE = 'image/jpeg'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
|
||||
_RND = SystemRandom()
|
||||
|
||||
CAMERA_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
def enable_motion_detection(hass, entity_id=None):
|
||||
"""Enable Motion Detection."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_EN_MOTION, data))
|
||||
|
||||
|
||||
def disable_motion_detection(hass, entity_id=None):
|
||||
"""Disable Motion Detection."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_DISEN_MOTION, data))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_image(hass, entity_id, timeout=10):
|
||||
@@ -92,6 +117,44 @@ def async_setup(hass, config):
|
||||
hass.async_add_job(entity.async_update_ha_state())
|
||||
|
||||
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_camera_service(service):
|
||||
"""Handle calls to the camera services."""
|
||||
target_cameras = component.async_extract_from_service(service)
|
||||
|
||||
for camera in target_cameras:
|
||||
if service.service == SERVICE_EN_MOTION:
|
||||
yield from camera.async_enable_motion_detection()
|
||||
elif service.service == SERVICE_DISEN_MOTION:
|
||||
yield from camera.async_disable_motion_detection()
|
||||
|
||||
update_tasks = []
|
||||
for camera in target_cameras:
|
||||
if not camera.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.async_add_job(
|
||||
camera.async_update_ha_state(True))
|
||||
if hasattr(camera, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_EN_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_EN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DISEN_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_DISEN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -101,6 +164,7 @@ class Camera(Entity):
|
||||
def __init__(self):
|
||||
"""Initialize a camera."""
|
||||
self.is_streaming = False
|
||||
self.content_type = DEFAULT_CONTENT_TYPE
|
||||
self.access_tokens = collections.deque([], 2)
|
||||
self.async_update_token()
|
||||
|
||||
@@ -124,6 +188,11 @@ class Camera(Entity):
|
||||
"""Return the camera brand."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
@@ -149,16 +218,17 @@ class Camera(Entity):
|
||||
response = web.StreamResponse()
|
||||
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--jpegboundary')
|
||||
'boundary=--frameboundary')
|
||||
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'
|
||||
'--frameboundary\r\n'
|
||||
'Content-Type: {}\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
|
||||
self.content_type, len(img_bytes)),
|
||||
'utf-8') + img_bytes + b'\r\n')
|
||||
|
||||
last_image = None
|
||||
|
||||
@@ -199,6 +269,22 @@ class Camera(Entity):
|
||||
else:
|
||||
return STATE_IDLE
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in the camera."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_enable_motion_detection(self):
|
||||
"""Call the job and enable motion detection."""
|
||||
return self.hass.async_add_job(self.enable_motion_detection)
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable motion detection in camera."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_disable_motion_detection(self):
|
||||
"""Call the job and disable motion detection."""
|
||||
return self.hass.async_add_job(self.disable_motion_detection)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the camera state attributes."""
|
||||
@@ -212,6 +298,9 @@ class Camera(Entity):
|
||||
if self.brand:
|
||||
attr['brand'] = self.brand
|
||||
|
||||
if self.motion_detection_enabled:
|
||||
attr['motion_detection'] = self.motion_detection_enabled
|
||||
|
||||
return attr
|
||||
|
||||
@callback
|
||||
@@ -269,7 +358,8 @@ class CameraImageView(CameraView):
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
if image:
|
||||
return web.Response(body=image, content_type='image/jpeg')
|
||||
return web.Response(body=image,
|
||||
content_type=camera.content_type)
|
||||
|
||||
return web.Response(status=500)
|
||||
|
||||
|
||||
@@ -6,32 +6,32 @@ https://home-assistant.io/components/camera.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND
|
||||
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up an Arlo IP Camera."""
|
||||
arlo = hass.data.get('arlo')
|
||||
arlo = hass.data.get(DATA_ARLO)
|
||||
if not arlo:
|
||||
return False
|
||||
|
||||
@@ -40,7 +40,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
cameras.append(ArloCam(hass, camera, config))
|
||||
|
||||
async_add_devices(cameras, True)
|
||||
return True
|
||||
|
||||
|
||||
class ArloCam(Camera):
|
||||
@@ -49,14 +48,15 @@ class ArloCam(Camera):
|
||||
def __init__(self, hass, camera, device_info):
|
||||
"""Initialize an Arlo camera."""
|
||||
super().__init__()
|
||||
|
||||
self._camera = camera
|
||||
self._base_stn = hass.data['arlo'].base_stations[0]
|
||||
self._name = self._camera.name
|
||||
self._motion_status = False
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
"""Return a still image response from the camera."""
|
||||
return self._camera.last_image
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -90,3 +90,27 @@ class ArloCam(Camera):
|
||||
def brand(self):
|
||||
"""Camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Camera should poll periodically."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Camera Motion Detection Status."""
|
||||
return self._motion_status
|
||||
|
||||
def set_base_station_mode(self, mode):
|
||||
"""Set the mode in the base station."""
|
||||
self._base_stn.mode = mode
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable the Motion detection in base station (Arm)."""
|
||||
self._motion_status = True
|
||||
self.set_base_station_mode(ARLO_MODE_ARMED)
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable the motion detection in base station (Disarm)."""
|
||||
self._motion_status = False
|
||||
self.set_base_station_mode(ARLO_MODE_DISARMED)
|
||||
|
||||
@@ -7,15 +7,16 @@ https://home-assistant.io/components/camera.axis/
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['axis']
|
||||
DOMAIN = 'axis'
|
||||
DEPENDENCIES = [DOMAIN]
|
||||
|
||||
|
||||
def _get_image_url(host, mode):
|
||||
@@ -27,12 +28,29 @@ def _get_image_url(host, mode):
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Axis camera."""
|
||||
device_info = {
|
||||
CONF_NAME: discovery_info['name'],
|
||||
CONF_USERNAME: discovery_info['username'],
|
||||
CONF_PASSWORD: discovery_info['password'],
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'),
|
||||
config = {
|
||||
CONF_NAME: discovery_info[CONF_NAME],
|
||||
CONF_USERNAME: discovery_info[CONF_USERNAME],
|
||||
CONF_PASSWORD: discovery_info[CONF_PASSWORD],
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST], 'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info[CONF_HOST],
|
||||
'single'),
|
||||
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
|
||||
}
|
||||
add_devices([MjpegCamera(hass, device_info)])
|
||||
add_devices([AxisCamera(hass, config)])
|
||||
|
||||
|
||||
class AxisCamera(MjpegCamera):
|
||||
"""AxisCamera class."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize Axis Communications camera component."""
|
||||
super().__init__(hass, config)
|
||||
async_dispatcher_connect(hass,
|
||||
DOMAIN + '_' + config[CONF_NAME] + '_new_ip',
|
||||
self._new_ip)
|
||||
|
||||
def _new_ip(self, host):
|
||||
"""Set new IP for video stream."""
|
||||
self._mjpeg_url = _get_image_url(host, 'mjpeg')
|
||||
self._still_image_url = _get_image_url(host, 'mjpeg')
|
||||
|
||||
@@ -5,25 +5,29 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import os
|
||||
|
||||
import logging
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo camera platform."""
|
||||
add_devices([
|
||||
DemoCamera('Demo camera')
|
||||
DemoCamera(hass, config, 'Demo camera')
|
||||
])
|
||||
|
||||
|
||||
class DemoCamera(Camera):
|
||||
"""The representation of a Demo camera."""
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, hass, config, name):
|
||||
"""Initialize demo camera component."""
|
||||
super().__init__()
|
||||
self._parent = hass
|
||||
self._name = name
|
||||
self._motion_status = False
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a faked still image response."""
|
||||
@@ -38,3 +42,21 @@ class DemoCamera(Camera):
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Camera should poll periodically."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Camera Motion Detection Status."""
|
||||
return self._motion_status
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable the Motion detection in base station (Arm)."""
|
||||
self._motion_status = True
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable the motion detection in base station (Disarm)."""
|
||||
self._motion_status = False
|
||||
|
||||
@@ -17,13 +17,15 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.components.camera import (
|
||||
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CONTENT_TYPE = 'content_type'
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
|
||||
CONF_STILL_IMAGE_URL = 'still_image_url'
|
||||
|
||||
@@ -37,6 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -59,6 +62,7 @@ class GenericCamera(Camera):
|
||||
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
||||
self._still_image_url.hass = hass
|
||||
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
|
||||
self.content_type = device_info[CONF_CONTENT_TYPE]
|
||||
|
||||
username = device_info.get(CONF_USERNAME)
|
||||
password = device_info.get(CONF_PASSWORD)
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.local_file/
|
||||
"""
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -46,6 +47,10 @@ class LocalFile(Camera):
|
||||
|
||||
self._name = name
|
||||
self._file_path = file_path
|
||||
# Set content type of local file
|
||||
content, _ = mimetypes.guess_type(file_path)
|
||||
if content is not None:
|
||||
self.content_type = content
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
|
||||
102
homeassistant/components/camera/onvif.py
Normal file
102
homeassistant/components/camera/onvif.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Support for ONVIF Cameras with FFmpeg as decoder.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.onvif/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import (
|
||||
DATA_FFMPEG)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['onvif-py3==0.1.3',
|
||||
'suds-py3==1.3.3.0',
|
||||
'http://github.com/tgaugry/suds-passworddigest-py3'
|
||||
'/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip'
|
||||
'#suds-passworddigest-py3==0.1.2a']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
DEFAULT_NAME = 'ONVIF Camera'
|
||||
DEFAULT_PORT = 5000
|
||||
DEFAULT_USERNAME = 'admin'
|
||||
DEFAULT_PASSWORD = '888888'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up a ONVIF camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)):
|
||||
return
|
||||
async_add_devices([ONVIFCamera(hass, config)])
|
||||
|
||||
|
||||
class ONVIFCamera(Camera):
|
||||
"""An implementation of an ONVIF camera."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a ONVIF camera."""
|
||||
from onvif import ONVIFService
|
||||
super().__init__()
|
||||
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._ffmpeg_arguments = '-q:v 2'
|
||||
media = ONVIFService(
|
||||
'http://{}:{}/onvif/device_service'.format(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT)),
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
'{}/deps/onvif/wsdl/media.wsdl'.format(hass.config.config_dir)
|
||||
)
|
||||
self._input = media.GetStreamUri().Uri
|
||||
_LOGGER.debug("ONVIF Camera Using the following URL for %s: %s",
|
||||
self._name, self._input)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
ffmpeg = ImageFrame(
|
||||
self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
|
||||
|
||||
image = yield from ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._ffmpeg_arguments)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary,
|
||||
loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
self._input, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
17
homeassistant/components/camera/services.yaml
Normal file
17
homeassistant/components/camera/services.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
# Describes the format for available camera services
|
||||
|
||||
enable_motion_detection:
|
||||
description: Enable the motion detection in a camera
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to enable motion detection
|
||||
example: 'camera.living_room_camera'
|
||||
|
||||
disable_motion_detection:
|
||||
description: Disable the motion detection in a camera
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to disable motion detection
|
||||
example: 'camera.living_room_camera'
|
||||
@@ -24,22 +24,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if not os.access(directory_path, os.R_OK):
|
||||
_LOGGER.error("file path %s is not readable", directory_path)
|
||||
return False
|
||||
hub.update_smartcam()
|
||||
hub.update_overview()
|
||||
smartcams = []
|
||||
smartcams.extend([
|
||||
VerisureSmartcam(hass, value.deviceLabel, directory_path)
|
||||
for value in hub.smartcam_status.values()])
|
||||
VerisureSmartcam(hass, device_label, directory_path)
|
||||
for device_label in hub.get(
|
||||
"$.customerImageCameras[*].deviceLabel")])
|
||||
add_devices(smartcams)
|
||||
|
||||
|
||||
class VerisureSmartcam(Camera):
|
||||
"""Representation of a Verisure camera."""
|
||||
|
||||
def __init__(self, hass, device_id, directory_path):
|
||||
def __init__(self, hass, device_label, directory_path):
|
||||
"""Initialize Verisure File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._device_id = device_id
|
||||
self._device_label = device_label
|
||||
self._directory_path = directory_path
|
||||
self._image = None
|
||||
self._image_id = None
|
||||
@@ -58,28 +59,27 @@ class VerisureSmartcam(Camera):
|
||||
|
||||
def check_imagelist(self):
|
||||
"""Check the contents of the image list."""
|
||||
hub.update_smartcam_imagelist()
|
||||
if (self._device_id not in hub.smartcam_dict or
|
||||
not hub.smartcam_dict[self._device_id]):
|
||||
hub.update_smartcam_imageseries()
|
||||
image_ids = hub.get_image_info(
|
||||
"$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId",
|
||||
self._device_label)
|
||||
if not image_ids:
|
||||
return
|
||||
images = hub.smartcam_dict[self._device_id]
|
||||
new_image_id = images[0]
|
||||
_LOGGER.debug("self._device_id=%s, self._images=%s, "
|
||||
"self._new_image_id=%s", self._device_id,
|
||||
images, new_image_id)
|
||||
new_image_id = image_ids[0]
|
||||
if (new_image_id == '-1' or
|
||||
self._image_id == new_image_id):
|
||||
_LOGGER.debug("The image is the same, or loading image_id")
|
||||
return
|
||||
_LOGGER.debug("Download new image %s", new_image_id)
|
||||
hub.my_pages.smartcam.download_image(
|
||||
self._device_id, new_image_id, self._directory_path)
|
||||
new_image_path = os.path.join(
|
||||
self._directory_path, '{}{}'.format(new_image_id, '.jpg'))
|
||||
hub.session.download_image(
|
||||
self._device_label, new_image_id, new_image_path)
|
||||
_LOGGER.debug("Old image_id=%s", self._image_id)
|
||||
self.delete_image(self)
|
||||
|
||||
self._image_id = new_image_id
|
||||
self._image = os.path.join(
|
||||
self._directory_path, '{}{}'.format(self._image_id, '.jpg'))
|
||||
self._image = new_image_path
|
||||
|
||||
def delete_image(self, event):
|
||||
"""Delete an old image."""
|
||||
@@ -95,4 +95,6 @@ class VerisureSmartcam(Camera):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return hub.smartcam_status[self._device_id].location
|
||||
return hub.get_first(
|
||||
"$.customerImageCameras[?(@.deviceLabel=='%s')].area",
|
||||
self._device_label)
|
||||
|
||||
@@ -693,8 +693,14 @@ class ClimateDevice(Entity):
|
||||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
if temp is None:
|
||||
return temp
|
||||
|
||||
# if the temperature is not a number this can cause issues
|
||||
# with polymer components, so bail early there.
|
||||
if not isinstance(temp, Number):
|
||||
raise TypeError("Temperature is not a number: %s" % temp)
|
||||
|
||||
if self.temperature_unit != self.unit_of_measurement:
|
||||
temp = convert_temperature(
|
||||
temp, self.temperature_unit, self.unit_of_measurement)
|
||||
|
||||
148
homeassistant/components/climate/flexit.py
Normal file
148
homeassistant/components/climate/flexit.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
Platform for Flexit AC units with CI66 Modbus adapter.
|
||||
|
||||
Example configuration:
|
||||
|
||||
climate:
|
||||
- platform: flexit
|
||||
name: Main AC
|
||||
slave: 21
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/climate.flexit/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_SLAVE, TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
import homeassistant.components.modbus as modbus
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyflexit==0.3']
|
||||
DEPENDENCIES = ['modbus']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)),
|
||||
vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Flexit Platform."""
|
||||
modbus_slave = config.get(CONF_SLAVE, None)
|
||||
name = config.get(CONF_NAME, None)
|
||||
add_devices([Flexit(modbus_slave, name)], True)
|
||||
|
||||
|
||||
class Flexit(ClimateDevice):
|
||||
"""Representation of a Flexit AC unit."""
|
||||
|
||||
def __init__(self, modbus_slave, name):
|
||||
"""Initialize the unit."""
|
||||
from pyflexit import pyflexit
|
||||
self._name = name
|
||||
self._slave = modbus_slave
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_fan_mode = None
|
||||
self._current_operation = None
|
||||
self._fan_list = ['Off', 'Low', 'Medium', 'High']
|
||||
self._current_operation = None
|
||||
self._filter_hours = None
|
||||
self._filter_alarm = None
|
||||
self._heat_recovery = None
|
||||
self._heater_enabled = False
|
||||
self._heating = None
|
||||
self._cooling = None
|
||||
self._alarm = False
|
||||
self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave)
|
||||
|
||||
def update(self):
|
||||
"""Update unit attributes."""
|
||||
if not self.unit.update():
|
||||
_LOGGER.warning("Modbus read failed")
|
||||
|
||||
self._target_temperature = self.unit.get_target_temp
|
||||
self._current_temperature = self.unit.get_temp
|
||||
self._current_fan_mode =\
|
||||
self._fan_list[self.unit.get_fan_speed]
|
||||
self._filter_hours = self.unit.get_filter_hours
|
||||
# Mechanical heat recovery, 0-100%
|
||||
self._heat_recovery = self.unit.get_heat_recovery
|
||||
# Heater active 0-100%
|
||||
self._heating = self.unit.get_heating
|
||||
# Cooling active 0-100%
|
||||
self._cooling = self.unit.get_cooling
|
||||
# Filter alarm 0/1
|
||||
self._filter_alarm = self.unit.get_filter_alarm
|
||||
# Heater enabled or not. Does not mean it's necessarily heating
|
||||
self._heater_enabled = self.unit.get_heater_enabled
|
||||
# Current operation mode
|
||||
self._current_operation = self.unit.get_operation
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
return {
|
||||
'filter_hours': self._filter_hours,
|
||||
'filter_alarm': self._filter_alarm,
|
||||
'heat_recovery': self._heat_recovery,
|
||||
'heating': self._heating,
|
||||
'heater_enabled': self._heater_enabled,
|
||||
'cooling': self._cooling
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the climate device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.unit.set_temp(self._target_temperature)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new fan mode."""
|
||||
self.unit.set_fan_speed(fan)
|
||||
@@ -67,7 +67,12 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._values.get(self.gateway.const.SetReq.V_TEMP)
|
||||
value = self._values.get(self.gateway.const.SetReq.V_TEMP)
|
||||
|
||||
if value is not None:
|
||||
value = float(value)
|
||||
|
||||
return value
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
@@ -79,21 +84,21 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
|
||||
if temp is None:
|
||||
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
|
||||
return temp
|
||||
return float(temp)
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_HVAC_SETPOINT_HEAT in self._values:
|
||||
return self._values.get(set_req.V_HVAC_SETPOINT_COOL)
|
||||
return float(self._values.get(set_req.V_HVAC_SETPOINT_COOL))
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_HVAC_SETPOINT_COOL in self._values:
|
||||
return self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
|
||||
return float(self._values.get(set_req.V_HVAC_SETPOINT_HEAT))
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['radiotherm==1.2']
|
||||
REQUIREMENTS = ['radiotherm==1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -84,6 +84,7 @@ class RadioThermostat(ClimateDevice):
|
||||
self._name = None
|
||||
self._fmode = None
|
||||
self._tmode = None
|
||||
self._tstate = None
|
||||
self._hold_temp = hold_temp
|
||||
self._away = False
|
||||
self._away_temps = away_temps
|
||||
@@ -140,6 +141,7 @@ class RadioThermostat(ClimateDevice):
|
||||
self._name = self.device.name['raw']
|
||||
self._fmode = self.device.fmode['human']
|
||||
self._tmode = self.device.tmode['human']
|
||||
self._tstate = self.device.tstate['human']
|
||||
|
||||
if self._tmode == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
@@ -147,6 +149,12 @@ class RadioThermostat(ClimateDevice):
|
||||
elif self._tmode == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
self._current_operation = STATE_HEAT
|
||||
elif self._tmode == 'Auto':
|
||||
if self._tstate == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
elif self._tstate == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
self._current_operation = STATE_AUTO
|
||||
else:
|
||||
self._current_operation = STATE_IDLE
|
||||
|
||||
@@ -159,6 +167,12 @@ class RadioThermostat(ClimateDevice):
|
||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||
elif self._current_operation == STATE_HEAT:
|
||||
self.device.t_heat = round(temperature * 2.0) / 2.0
|
||||
elif self._current_operation == STATE_AUTO:
|
||||
if self._tstate == 'Cool':
|
||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||
elif self._tstate == 'Heat':
|
||||
self.device.t_heat = round(temperature * 2.0) / 2.0
|
||||
|
||||
if self._hold_temp or self._away:
|
||||
self.device.hold = 1
|
||||
else:
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
@@ -52,9 +53,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)):
|
||||
if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]:
|
||||
devices.append(SensiboClimate(client, dev))
|
||||
except aiohttp.client_exceptions.ClientConnectorError:
|
||||
except (aiohttp.client_exceptions.ClientConnectorError,
|
||||
asyncio.TimeoutError):
|
||||
_LOGGER.exception('Failed to connct to Sensibo servers.')
|
||||
return False
|
||||
raise PlatformNotReady
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
||||
@@ -24,6 +24,17 @@ CONST_OVERLAY_MANUAL = 'MANUAL'
|
||||
# the temperature will be reset after a timespan
|
||||
CONST_OVERLAY_TIMER = 'TIMER'
|
||||
|
||||
CONST_MODE_FAN_HIGH = 'HIGH'
|
||||
CONST_MODE_FAN_MIDDLE = 'MIDDLE'
|
||||
CONST_MODE_FAN_LOW = 'LOW'
|
||||
|
||||
FAN_MODES_LIST = {
|
||||
CONST_MODE_FAN_HIGH: 'High',
|
||||
CONST_MODE_FAN_MIDDLE: 'Middle',
|
||||
CONST_MODE_FAN_LOW: 'Low',
|
||||
CONST_MODE_OFF: 'Off',
|
||||
}
|
||||
|
||||
OPERATION_LIST = {
|
||||
CONST_OVERLAY_MANUAL: 'Manual',
|
||||
CONST_OVERLAY_TIMER: 'Timer',
|
||||
@@ -60,9 +71,15 @@ def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
min_temp = float(capabilities['temperatures']['celsius']['min'])
|
||||
max_temp = float(capabilities['temperatures']['celsius']['max'])
|
||||
ac_mode = capabilities['type'] != 'HEATING'
|
||||
ac_mode = capabilities['type'] == 'AIR_CONDITIONING'
|
||||
|
||||
if ac_mode:
|
||||
temperatures = capabilities['HEAT']['temperatures']
|
||||
else:
|
||||
temperatures = capabilities['temperatures']
|
||||
|
||||
min_temp = float(temperatures['celsius']['min'])
|
||||
max_temp = float(temperatures['celsius']['max'])
|
||||
|
||||
data_id = 'zone {} {}'.format(name, zone_id)
|
||||
device = TadoClimate(tado,
|
||||
@@ -107,7 +124,9 @@ class TadoClimate(ClimateDevice):
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = None
|
||||
self._tolerance = tolerance
|
||||
self._cooling = False
|
||||
|
||||
self._current_fan = CONST_MODE_OFF
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
@@ -129,13 +148,32 @@ class TadoClimate(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current readable operation mode."""
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
if self._cooling:
|
||||
return "Cooling"
|
||||
else:
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes (readable)."""
|
||||
return list(OPERATION_LIST.values())
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
if self.ac_mode:
|
||||
return FAN_MODES_LIST.get(self._current_fan)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
if self.ac_mode:
|
||||
return list(FAN_MODES_LIST.values())
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
@@ -205,27 +243,27 @@ class TadoClimate(ClimateDevice):
|
||||
|
||||
if 'sensorDataPoints' in data:
|
||||
sensor_data = data['sensorDataPoints']
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
humidity = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
setting = 0
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
if 'insideTemperature' in sensor_data:
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
self._cur_temp = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
if 'humidity' in sensor_data:
|
||||
humidity = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
self._cur_humidity = humidity
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if 'temperature' in data['setting'] and \
|
||||
data['setting']['temperature'] is not None:
|
||||
setting = float(
|
||||
data['setting']['temperature']['celsius'])
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
self._cur_temp = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
self._cur_humidity = humidity
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
if 'tadoMode' in data:
|
||||
mode = data['tadoMode']
|
||||
@@ -235,29 +273,39 @@ class TadoClimate(ClimateDevice):
|
||||
power = data['setting']['power']
|
||||
if power == 'OFF':
|
||||
self._current_operation = CONST_MODE_OFF
|
||||
self._current_fan = CONST_MODE_OFF
|
||||
# There is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._device_is_active = False
|
||||
else:
|
||||
self._device_is_active = True
|
||||
|
||||
if 'overlay' in data and data['overlay'] is not None:
|
||||
overlay = True
|
||||
termination = data['overlay']['termination']['type']
|
||||
else:
|
||||
if self._device_is_active:
|
||||
overlay = False
|
||||
termination = ""
|
||||
overlay_data = None
|
||||
termination = self._current_operation
|
||||
cooling = False
|
||||
fan_speed = CONST_MODE_OFF
|
||||
|
||||
# If you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
if 'overlay' in data:
|
||||
overlay_data = data['overlay']
|
||||
overlay = overlay_data is not None
|
||||
|
||||
if overlay:
|
||||
termination = overlay_data['termination']['type']
|
||||
|
||||
if 'setting' in overlay_data:
|
||||
cooling = overlay_data['setting']['mode'] == 'COOL'
|
||||
fan_speed = overlay_data['setting']['fanSpeed']
|
||||
|
||||
# If you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
|
||||
if overlay and self._device_is_active:
|
||||
# There is an overlay the device is on
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
else:
|
||||
# There is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._cooling = cooling
|
||||
self._current_fan = fan_speed
|
||||
|
||||
def _control_heating(self):
|
||||
"""Send new target temperature to mytado."""
|
||||
|
||||
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([WinkAC(climate, hass, temp_unit)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method,too-many-public-methods, too-many-branches
|
||||
# pylint: disable=abstract-method
|
||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink thermostat."""
|
||||
|
||||
|
||||
134
homeassistant/components/comfoconnect.py
Normal file
134
homeassistant/components/comfoconnect.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/comfoconnect/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_TOKEN, CONF_PIN, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import (discovery)
|
||||
from homeassistant.helpers.dispatcher import (dispatcher_send)
|
||||
|
||||
REQUIREMENTS = ['pycomfoconnect==0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'comfoconnect'
|
||||
|
||||
SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = 'comfoconnect_update_received'
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
|
||||
ATTR_CURRENT_HUMIDITY = 'current_humidity'
|
||||
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
|
||||
ATTR_OUTSIDE_HUMIDITY = 'outside_humidity'
|
||||
ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply'
|
||||
ATTR_AIR_FLOW_EXHAUST = 'air_flow_exhaust'
|
||||
|
||||
CONF_USER_AGENT = 'user_agent'
|
||||
|
||||
DEFAULT_NAME = 'ComfoAirQ'
|
||||
DEFAULT_PIN = 0
|
||||
DEFAULT_TOKEN = '00000000000000000000000000000001'
|
||||
DEFAULT_USER_AGENT = 'Home Assistant'
|
||||
|
||||
DEVICE = None
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN):
|
||||
vol.Length(min=32, max=32, msg='invalid token'),
|
||||
vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): cv.string,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the ComfoConnect bridge."""
|
||||
from pycomfoconnect import (Bridge)
|
||||
|
||||
conf = config[DOMAIN]
|
||||
host = conf.get(CONF_HOST)
|
||||
name = conf.get(CONF_NAME)
|
||||
token = conf.get(CONF_TOKEN)
|
||||
user_agent = conf.get(CONF_USER_AGENT)
|
||||
pin = conf.get(CONF_PIN)
|
||||
|
||||
# Run discovery on the configured ip
|
||||
bridges = Bridge.discover(host)
|
||||
if not bridges:
|
||||
_LOGGER.error("Could not connect to ComfoConnect bridge on %s", host)
|
||||
return False
|
||||
bridge = bridges[0]
|
||||
_LOGGER.info("Bridge found: %s (%s)", bridge.uuid.hex(), bridge.host)
|
||||
|
||||
# Setup ComfoConnect Bridge
|
||||
ccb = ComfoConnectBridge(hass, bridge, name, token, user_agent, pin)
|
||||
hass.data[DOMAIN] = ccb
|
||||
|
||||
# Start connection with bridge
|
||||
ccb.connect()
|
||||
|
||||
# Schedule disconnect on shutdown
|
||||
def _shutdown(_event):
|
||||
ccb.disconnect()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
|
||||
|
||||
# Load platforms
|
||||
discovery.load_platform(hass, 'fan', DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ComfoConnectBridge(object):
|
||||
"""Representation of a ComfoConnect bridge."""
|
||||
|
||||
def __init__(self, hass, bridge, name, token, friendly_name, pin):
|
||||
"""Initialize the ComfoConnect bridge."""
|
||||
from pycomfoconnect import (ComfoConnect)
|
||||
|
||||
self.data = {}
|
||||
self.name = name
|
||||
self.hass = hass
|
||||
|
||||
self.comfoconnect = ComfoConnect(
|
||||
bridge=bridge, local_uuid=bytes.fromhex(token),
|
||||
local_devicename=friendly_name, pin=pin)
|
||||
self.comfoconnect.callback_sensor = self.sensor_callback
|
||||
|
||||
def connect(self):
|
||||
"""Connect with the bridge."""
|
||||
_LOGGER.debug("Connecting with bridge")
|
||||
self.comfoconnect.connect(True)
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from the bridge."""
|
||||
_LOGGER.debug("Disconnecting from bridge")
|
||||
self.comfoconnect.disconnect()
|
||||
|
||||
def sensor_callback(self, var, value):
|
||||
"""Callback function for sensor updates."""
|
||||
_LOGGER.debug("Got value from bridge: %d = %d", var, value)
|
||||
|
||||
from pycomfoconnect import (
|
||||
SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR)
|
||||
|
||||
if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]:
|
||||
self.data[var] = value / 10
|
||||
else:
|
||||
self.data[var] = value
|
||||
|
||||
# Notify listeners that we have received an update
|
||||
dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var)
|
||||
|
||||
def subscribe_sensor(self, sensor_id):
|
||||
"""Subscribe for the specified sensor."""
|
||||
self.comfoconnect.register_sensor(sensor_id)
|
||||
@@ -14,11 +14,13 @@ from homeassistant import core
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import script
|
||||
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.15.0']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
ATTR_SENTENCE = 'sentence'
|
||||
DOMAIN = 'conversation'
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
@@ -29,9 +31,12 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
|
||||
cv.string: vol.Schema({
|
||||
vol.Required(ATTR_SENTENCE): cv.string,
|
||||
vol.Required('action'): cv.SCRIPT_SCHEMA,
|
||||
})
|
||||
})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@@ -40,9 +45,30 @@ def setup(hass, config):
|
||||
from fuzzywuzzy import process as fuzzyExtract
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
config = config.get(DOMAIN, {})
|
||||
|
||||
choices = {attrs[ATTR_SENTENCE]: script.Script(
|
||||
hass,
|
||||
attrs['action'],
|
||||
name)
|
||||
for name, attrs in config.items()}
|
||||
|
||||
def process(service):
|
||||
"""Parse text into commands."""
|
||||
# if actually configured
|
||||
if choices:
|
||||
text = service.data[ATTR_TEXT]
|
||||
match = fuzzyExtract.extractOne(text, choices.keys())
|
||||
scorelimit = 60 # arbitrary value
|
||||
logging.info(
|
||||
'matched up text %s and found %s',
|
||||
text,
|
||||
[match[0] if match[1] > scorelimit else 'nothing']
|
||||
)
|
||||
if match[1] > scorelimit:
|
||||
choices[match[0]].run() # run respective script
|
||||
return
|
||||
|
||||
text = service.data[ATTR_TEXT]
|
||||
match = REGEX_TURN_COMMAND.match(text)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from homeassistant.const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'cover'
|
||||
DEPENDENCIES = ['group']
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
GROUP_NAME_ALL_COVERS = 'all covers'
|
||||
@@ -39,6 +40,8 @@ DEVICE_CLASSES = [
|
||||
'garage', # Garage door control
|
||||
]
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
SUPPORT_OPEN = 1
|
||||
SUPPORT_CLOSE = 2
|
||||
SUPPORT_SET_POSITION = 4
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
The homematic cover platform.
|
||||
The HomeMatic cover platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.homematic/
|
||||
@@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class HMCover(HMDevice, CoverDevice):
|
||||
"""Representation a Homematic Cover."""
|
||||
"""Representation a HomeMatic Cover."""
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
@@ -70,7 +70,6 @@ class HMCover(HMDevice, CoverDevice):
|
||||
self._hmdevice.stop(self._channel)
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from hm metadata."""
|
||||
# Add state to data dict
|
||||
"""Generate a data dictoinary (self._data) from metadata."""
|
||||
self._state = "LEVEL"
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
|
||||
185
homeassistant/components/cover/knx.py
Normal file
185
homeassistant/components/cover/knx.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Support for KNX covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.knx/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, SUPPORT_STOP,
|
||||
SUPPORT_SET_TILT_POSITION
|
||||
)
|
||||
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
|
||||
from homeassistant.const import (CONF_NAME, CONF_DEVICE_CLASS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_GETPOSITION_ADDRESS = 'getposition_address'
|
||||
CONF_SETPOSITION_ADDRESS = 'setposition_address'
|
||||
CONF_GETANGLE_ADDRESS = 'getangle_address'
|
||||
CONF_SETANGLE_ADDRESS = 'setangle_address'
|
||||
CONF_STOP = 'stop_address'
|
||||
CONF_UPDOWN = 'updown_address'
|
||||
CONF_INVERT_POSITION = 'invert_position'
|
||||
CONF_INVERT_ANGLE = 'invert_angle'
|
||||
|
||||
DEFAULT_NAME = 'KNX Cover'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_UPDOWN): cv.string,
|
||||
vol.Required(CONF_STOP): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_GETPOSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SETPOSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
|
||||
vol.Inclusive(CONF_GETANGLE_ADDRESS, 'angle'): cv.string,
|
||||
vol.Inclusive(CONF_SETANGLE_ADDRESS, 'angle'): cv.string,
|
||||
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
add_devices([KNXCover(hass, KNXConfig(config))])
|
||||
|
||||
|
||||
class KNXCover(KNXMultiAddressDevice, CoverDevice):
|
||||
"""Representation of a KNX cover. e.g. a rollershutter."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the cover."""
|
||||
KNXMultiAddressDevice.__init__(
|
||||
self, hass, config,
|
||||
['updown', 'stop'], # required
|
||||
optional=['setposition', 'getposition',
|
||||
'getangle', 'setangle']
|
||||
)
|
||||
self._device_class = config.config.get(CONF_DEVICE_CLASS)
|
||||
self._invert_position = config.config.get(CONF_INVERT_POSITION)
|
||||
self._invert_angle = config.config.get(CONF_INVERT_ANGLE)
|
||||
self._hass = hass
|
||||
self._current_pos = None
|
||||
self._target_pos = None
|
||||
self._current_tilt = None
|
||||
self._target_tilt = None
|
||||
self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
|
||||
SUPPORT_SET_POSITION | SUPPORT_STOP
|
||||
|
||||
# Tilt is only supported, if there is a angle get and set address
|
||||
if CONF_SETANGLE_ADDRESS in config.config:
|
||||
_LOGGER.debug("%s: Tilt supported at addresses %s, %s",
|
||||
self.name, config.config.get(CONF_SETANGLE_ADDRESS),
|
||||
config.config.get(CONF_GETANGLE_ADDRESS))
|
||||
self._supported_features = self._supported_features | \
|
||||
SUPPORT_SET_TILT_POSITION
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is needed for the KNX cover."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._current_pos
|
||||
|
||||
@property
|
||||
def target_position(self):
|
||||
"""Return the position we are trying to reach: 0 - 100."""
|
||||
return self._target_pos
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._current_tilt
|
||||
|
||||
@property
|
||||
def target_tilt(self):
|
||||
"""Return the tilt angle (in %) we are trying to reach: 0 - 100."""
|
||||
return self._target_tilt
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Set new target position."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
if self._invert_position:
|
||||
position = 100-position
|
||||
|
||||
self._target_pos = position
|
||||
self.set_percentage('setposition', position)
|
||||
_LOGGER.debug("%s: Set target position to %d", self.name, position)
|
||||
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
super().update()
|
||||
value = self.get_percentage('getposition')
|
||||
if value is not None:
|
||||
self._current_pos = value
|
||||
if self._invert_position:
|
||||
self._current_pos = 100-value
|
||||
_LOGGER.debug("%s: position = %d", self.name, value)
|
||||
|
||||
if self._supported_features & SUPPORT_SET_TILT_POSITION:
|
||||
value = self.get_percentage('getangle')
|
||||
if value is not None:
|
||||
self._current_tilt = value
|
||||
if self._invert_angle:
|
||||
self._current_tilt = 100-value
|
||||
_LOGGER.debug("%s: tilt = %d", self.name, value)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
_LOGGER.debug("%s: open: updown = 0", self.name)
|
||||
self.set_int_value('updown', 0)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
_LOGGER.debug("%s: open: updown = 1", self.name)
|
||||
self.set_int_value('updown', 1)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover movement."""
|
||||
_LOGGER.debug("%s: stop: stop = 1", self.name)
|
||||
self.set_int_value('stop', 1)
|
||||
|
||||
def set_cover_tilt_position(self, tilt_position, **kwargs):
|
||||
"""Move the cover til to a specific position."""
|
||||
if self._invert_angle:
|
||||
tilt_position = 100-tilt_position
|
||||
|
||||
self._target_tilt = round(tilt_position, -1)
|
||||
self.set_percentage('setangle', tilt_position)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
@@ -14,7 +14,9 @@ import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, ATTR_TILT_POSITION, SUPPORT_OPEN_TILT,
|
||||
SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION,
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION)
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION,
|
||||
ATTR_POSITION)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
|
||||
STATE_CLOSED, STATE_UNKNOWN)
|
||||
@@ -29,6 +31,8 @@ DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_TILT_COMMAND_TOPIC = 'tilt_command_topic'
|
||||
CONF_TILT_STATUS_TOPIC = 'tilt_status_topic'
|
||||
CONF_POSITION_TOPIC = 'set_position_topic'
|
||||
CONF_SET_POSITION_TEMPLATE = 'set_position_template'
|
||||
|
||||
CONF_PAYLOAD_OPEN = 'payload_open'
|
||||
CONF_PAYLOAD_CLOSE = 'payload_close'
|
||||
@@ -55,10 +59,17 @@ DEFAULT_TILT_MAX = 100
|
||||
DEFAULT_TILT_OPTIMISTIC = False
|
||||
DEFAULT_TILT_INVERT_STATE = False
|
||||
|
||||
OPEN_CLOSE_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP)
|
||||
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_COMMAND_TOPIC, default=None): valid_publish_topic,
|
||||
vol.Optional(CONF_POSITION_TOPIC, default=None): valid_publish_topic,
|
||||
vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
|
||||
@@ -87,6 +98,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
set_position_template = config.get(CONF_SET_POSITION_TEMPLATE)
|
||||
if set_position_template is not None:
|
||||
set_position_template.hass = hass
|
||||
|
||||
async_add_devices([MqttCover(
|
||||
config.get(CONF_NAME),
|
||||
@@ -109,6 +123,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_TILT_MAX),
|
||||
config.get(CONF_TILT_STATE_OPTIMISTIC),
|
||||
config.get(CONF_TILT_INVERT_STATE),
|
||||
config.get(CONF_POSITION_TOPIC),
|
||||
set_position_template,
|
||||
)])
|
||||
|
||||
|
||||
@@ -120,7 +136,7 @@ class MqttCover(CoverDevice):
|
||||
payload_open, payload_close, payload_stop,
|
||||
optimistic, value_template, tilt_open_position,
|
||||
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
|
||||
tilt_invert):
|
||||
tilt_invert, position_topic, set_position_template):
|
||||
"""Initialize the cover."""
|
||||
self._position = None
|
||||
self._state = None
|
||||
@@ -145,6 +161,8 @@ class MqttCover(CoverDevice):
|
||||
self._tilt_max = tilt_max
|
||||
self._tilt_optimistic = tilt_optimistic
|
||||
self._tilt_invert = tilt_invert
|
||||
self._position_topic = position_topic
|
||||
self._set_position_template = set_position_template
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
@@ -233,9 +251,11 @@ class MqttCover(CoverDevice):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
supported_features = 0
|
||||
if self._command_topic is not None:
|
||||
supported_features = OPEN_CLOSE_FEATURES
|
||||
|
||||
if self.current_cover_position is not None:
|
||||
if self._position_topic is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
if self._tilt_command_topic is not None:
|
||||
@@ -315,6 +335,22 @@ class MqttCover(CoverDevice):
|
||||
mqtt.async_publish(self.hass, self._tilt_command_topic,
|
||||
level, self._qos, self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
if self._set_position_template is not None:
|
||||
try:
|
||||
position = self._set_position_template.async_render(
|
||||
**kwargs)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._state = None
|
||||
|
||||
mqtt.async_publish(self.hass, self._position_topic,
|
||||
position, self._qos, self._retain)
|
||||
|
||||
def find_percentage_in_range(self, position):
|
||||
"""Find the 0-100% value within the specified range."""
|
||||
# the range of motion as defined by the min max values
|
||||
|
||||
@@ -14,9 +14,7 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
|
||||
'#pymyq==0.0.8']
|
||||
REQUIREMENTS = ['pymyq==0.0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
354
homeassistant/components/cover/template.py
Normal file
354
homeassistant/components/cover/template.py
Normal file
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
Support for covers which integrate with other components.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.template/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.cover import (
|
||||
ENTITY_ID_FORMAT, CoverDevice, PLATFORM_SCHEMA,
|
||||
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT,
|
||||
SUPPORT_SET_TILT_POSITION, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
|
||||
SUPPORT_SET_POSITION, ATTR_POSITION, ATTR_TILT_POSITION)
|
||||
from homeassistant.const import (
|
||||
CONF_FRIENDLY_NAME, CONF_ENTITY_ID,
|
||||
EVENT_HOMEASSISTANT_START, MATCH_ALL,
|
||||
CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE,
|
||||
STATE_OPEN, STATE_CLOSED)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.script import Script
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_VALID_STATES = [STATE_OPEN, STATE_CLOSED, 'true', 'false']
|
||||
|
||||
CONF_COVERS = 'covers'
|
||||
|
||||
CONF_POSITION_TEMPLATE = 'position_template'
|
||||
CONF_TILT_TEMPLATE = 'tilt_template'
|
||||
OPEN_ACTION = 'open_cover'
|
||||
CLOSE_ACTION = 'close_cover'
|
||||
STOP_ACTION = 'stop_cover'
|
||||
POSITION_ACTION = 'set_cover_position'
|
||||
TILT_ACTION = 'set_cover_tilt_position'
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position'
|
||||
|
||||
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(OPEN_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CLOSE_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(STOP_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Exclusive(CONF_POSITION_TEMPLATE,
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
|
||||
vol.Exclusive(CONF_VALUE_TEMPLATE,
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_POSITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TILT_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_ICON_TEMPLATE): cv.template,
|
||||
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Template cover."""
|
||||
covers = []
|
||||
|
||||
for device, device_config in config[CONF_COVERS].items():
|
||||
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
|
||||
state_template = device_config.get(CONF_VALUE_TEMPLATE)
|
||||
position_template = device_config.get(CONF_POSITION_TEMPLATE)
|
||||
tilt_template = device_config.get(CONF_TILT_TEMPLATE)
|
||||
icon_template = device_config.get(CONF_ICON_TEMPLATE)
|
||||
open_action = device_config[OPEN_ACTION]
|
||||
close_action = device_config[CLOSE_ACTION]
|
||||
stop_action = device_config[STOP_ACTION]
|
||||
position_action = device_config.get(POSITION_ACTION)
|
||||
tilt_action = device_config.get(TILT_ACTION)
|
||||
|
||||
if position_template is None and state_template is None:
|
||||
_LOGGER.error('Must specify either %s' or '%s',
|
||||
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE)
|
||||
continue
|
||||
|
||||
template_entity_ids = set()
|
||||
if state_template is not None:
|
||||
temp_ids = state_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if position_template is not None:
|
||||
temp_ids = position_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if tilt_template is not None:
|
||||
temp_ids = tilt_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if icon_template is not None:
|
||||
temp_ids = icon_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if not template_entity_ids:
|
||||
template_entity_ids = MATCH_ALL
|
||||
|
||||
entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids)
|
||||
|
||||
covers.append(
|
||||
CoverTemplate(
|
||||
hass,
|
||||
device, friendly_name, state_template,
|
||||
position_template, tilt_template, icon_template,
|
||||
open_action, close_action, stop_action,
|
||||
position_action, tilt_action, entity_ids
|
||||
)
|
||||
)
|
||||
if not covers:
|
||||
_LOGGER.error("No covers added")
|
||||
return False
|
||||
|
||||
async_add_devices(covers, True)
|
||||
return True
|
||||
|
||||
|
||||
class CoverTemplate(CoverDevice):
|
||||
"""Representation of a Template cover."""
|
||||
|
||||
def __init__(self, hass, device_id, friendly_name, state_template,
|
||||
position_template, tilt_template, icon_template,
|
||||
open_action, close_action, stop_action,
|
||||
position_action, tilt_action, entity_ids):
|
||||
"""Initialize the Template cover."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||
self._name = friendly_name
|
||||
self._template = state_template
|
||||
self._position_template = position_template
|
||||
self._tilt_template = tilt_template
|
||||
self._icon_template = icon_template
|
||||
self._open_script = Script(hass, open_action)
|
||||
self._close_script = Script(hass, close_action)
|
||||
self._stop_script = Script(hass, stop_action)
|
||||
self._position_script = None
|
||||
if position_action is not None:
|
||||
self._position_script = Script(hass, position_action)
|
||||
self._tilt_script = None
|
||||
if tilt_action is not None:
|
||||
self._tilt_script = Script(hass, tilt_action)
|
||||
self._icon = None
|
||||
self._position = None
|
||||
self._tilt_value = None
|
||||
self._entities = entity_ids
|
||||
|
||||
if self._template is not None:
|
||||
self._template.hass = self.hass
|
||||
if self._position_template is not None:
|
||||
self._position_template.hass = self.hass
|
||||
if self._tilt_template is not None:
|
||||
self._tilt_template.hass = self.hass
|
||||
if self._icon_template is not None:
|
||||
self._icon_template.hass = self.hass
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._position = 100 if state.state == STATE_OPEN else 0
|
||||
|
||||
@callback
|
||||
def template_cover_state_listener(entity, old_state, new_state):
|
||||
"""Handle target device state changes."""
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
@callback
|
||||
def template_cover_startup(event):
|
||||
"""Update template on startup."""
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_cover_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_cover_startup)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._position == 0
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._position
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current position of cover tilt.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._tilt_value
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
|
||||
if self.current_cover_position is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
if self.current_cover_tilt_position is not None:
|
||||
supported_features |= TILT_FEATURES
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
self.hass.async_add_job(self._open_script.async_run())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
self.hass.async_add_job(self._close_script.async_run())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
self.hass.async_add_job(self._stop_script.async_run())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Set cover position."""
|
||||
if ATTR_POSITION not in kwargs:
|
||||
return
|
||||
self._position = kwargs[ATTR_POSITION]
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": self._position}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover open."""
|
||||
self._tilt_value = 100
|
||||
self.hass.async_add_job(self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover closed."""
|
||||
self._tilt_value = 0
|
||||
self.hass.async_add_job(self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if ATTR_TILT_POSITION not in kwargs:
|
||||
return
|
||||
self._tilt_value = kwargs[ATTR_TILT_POSITION]
|
||||
self.hass.async_add_job(self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the state from the template."""
|
||||
if self._template is not None:
|
||||
try:
|
||||
state = self._template.async_render().lower()
|
||||
if state in _VALID_STATES:
|
||||
if state in ('true', STATE_OPEN):
|
||||
self._position = 100
|
||||
else:
|
||||
self._position = 0
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid cover is_on state: %s. Expected: %s',
|
||||
state, ', '.join(_VALID_STATES))
|
||||
self._position = None
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._position = None
|
||||
if self._position_template is not None:
|
||||
try:
|
||||
state = float(self._position_template.async_render())
|
||||
if state < 0 or state > 100:
|
||||
self._position = None
|
||||
_LOGGER.error("Cover position value must be"
|
||||
" between 0 and 100."
|
||||
" Value was: %.2f", state)
|
||||
else:
|
||||
self._position = state
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._position = None
|
||||
except ValueError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._position = None
|
||||
if self._tilt_template is not None:
|
||||
try:
|
||||
state = float(self._tilt_template.async_render())
|
||||
if state < 0 or state > 100:
|
||||
self._tilt_value = None
|
||||
_LOGGER.error("Tilt value must be between 0 and 100."
|
||||
" Value was: %.2f", state)
|
||||
else:
|
||||
self._tilt_value = state
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._tilt_value = None
|
||||
except ValueError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._tilt_value = None
|
||||
if self._icon_template is not None:
|
||||
try:
|
||||
self._icon = self._icon_template.async_render()
|
||||
except TemplateError as ex:
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
# Common during HA startup - so just a warning
|
||||
_LOGGER.warning('Could not render icon template %s,'
|
||||
' the state is unknown.', self._name)
|
||||
return
|
||||
self._icon = super().icon
|
||||
_LOGGER.error('Could not render icon template %s: %s',
|
||||
self._name, ex)
|
||||
@@ -210,6 +210,7 @@ def async_setup(hass, config):
|
||||
description=("Press the button on the bridge to register Philips "
|
||||
"Hue with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
fields=[{'id': 'username', 'name': 'Username'}],
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
configurator_ids.append(request_id)
|
||||
|
||||
@@ -27,6 +27,7 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -41,7 +42,7 @@ from homeassistant.const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'device_tracker'
|
||||
DEPENDENCIES = ['zone']
|
||||
DEPENDENCIES = ['zone', 'group']
|
||||
|
||||
GROUP_NAME_ALL_DEVICES = 'all devices'
|
||||
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
||||
@@ -122,15 +123,10 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Set up the device tracker."""
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
|
||||
try:
|
||||
conf = config.get(DOMAIN, [])
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, DOMAIN, config, hass)
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if conf else {}
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
conf = config.get(DOMAIN, [])
|
||||
conf = conf[0] if conf else {}
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
|
||||
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
||||
@@ -180,7 +176,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
if setup_tasks:
|
||||
yield from asyncio.wait(setup_tasks, loop=hass.loop)
|
||||
|
||||
yield from tracker.async_setup_group()
|
||||
tracker.async_setup_group()
|
||||
|
||||
@callback
|
||||
def async_device_tracker_discovered(service, info):
|
||||
@@ -233,7 +229,7 @@ class DeviceTracker(object):
|
||||
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.group = None
|
||||
self._is_updating = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
for dev in devices:
|
||||
@@ -246,18 +242,21 @@ class DeviceTracker(object):
|
||||
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,
|
||||
source_type: str=SOURCE_TYPE_GPS):
|
||||
source_type: str=SOURCE_TYPE_GPS, picture: str=None,
|
||||
icon: str=None):
|
||||
"""Notify the device tracker that you see a device."""
|
||||
self.hass.add_job(
|
||||
self.async_see(mac, dev_id, host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes, source_type)
|
||||
gps_accuracy, battery, attributes, source_type,
|
||||
picture, icon)
|
||||
)
|
||||
|
||||
@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,
|
||||
source_type: str=SOURCE_TYPE_GPS):
|
||||
source_type: str=SOURCE_TYPE_GPS, picture: str=None,
|
||||
icon: str=None):
|
||||
"""Notify the device tracker that you see a device.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -285,7 +284,8 @@ class DeviceTracker(object):
|
||||
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('_', ' '))
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '),
|
||||
picture=picture, icon=icon)
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
@@ -303,9 +303,10 @@ class DeviceTracker(object):
|
||||
})
|
||||
|
||||
# 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])
|
||||
if self.group and self.track_new:
|
||||
self.group.async_set_group(
|
||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
|
||||
|
||||
# lookup mac vendor string to be stored in config
|
||||
yield from device.set_vendor_for_mac()
|
||||
@@ -327,16 +328,19 @@ class DeviceTracker(object):
|
||||
update_config, self.hass.config.path(YAML_DEVICES),
|
||||
dev_id, device)
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def async_setup_group(self):
|
||||
"""Initialize group for all tracked devices.
|
||||
|
||||
This method is a coroutine.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||
if dev.track)
|
||||
self.group = yield from group.Group.async_create_group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
entity_ids = [dev.entity_id for dev in self.devices.values()
|
||||
if dev.track]
|
||||
|
||||
self.group = get_component('group')
|
||||
self.group.async_set_group(
|
||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||
name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids)
|
||||
|
||||
@callback
|
||||
def async_update_stale(self, now: dt_util.dt.datetime):
|
||||
|
||||
110
homeassistant/components/device_tracker/linksys_smart.py
Normal file
110
homeassistant/components/device_tracker/linksys_smart.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Support for Linksys Smart Wifi routers."""
|
||||
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, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Linksys AP scanner."""
|
||||
try:
|
||||
return LinksysSmartWifiDeviceScanner(config[DOMAIN])
|
||||
except ConnectionError:
|
||||
return None
|
||||
|
||||
|
||||
class LinksysSmartWifiDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Linksys Access Point."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
self.last_results = {}
|
||||
|
||||
# Check if the access point is accessible
|
||||
response = self._make_request()
|
||||
if not response.status_code == 200:
|
||||
raise ConnectionError("Cannot connect to Linksys Access Point")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with device IDs (MACs)."""
|
||||
self._update_info()
|
||||
|
||||
return self.last_results.keys()
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name (if known) of the device."""
|
||||
return self.last_results.get(mac)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Check for connected devices."""
|
||||
with self.lock:
|
||||
_LOGGER.info("Checking Linksys Smart Wifi")
|
||||
|
||||
self.last_results = {}
|
||||
response = self._make_request()
|
||||
if response.status_code != 200:
|
||||
_LOGGER.error(
|
||||
"Got HTTP status code %d when getting device list",
|
||||
response.status_code)
|
||||
return False
|
||||
try:
|
||||
data = response.json()
|
||||
result = data["responses"][0]
|
||||
devices = result["output"]["devices"]
|
||||
for device in devices:
|
||||
macs = device["knownMACAddresses"]
|
||||
if not macs:
|
||||
_LOGGER.warning(
|
||||
"Skipping device without known MAC address")
|
||||
continue
|
||||
mac = macs[-1]
|
||||
connections = device["connections"]
|
||||
if not connections:
|
||||
_LOGGER.debug("Device %s is not connected", mac)
|
||||
continue
|
||||
name = device["friendlyName"]
|
||||
properties = device["properties"]
|
||||
for prop in properties:
|
||||
if prop["name"] == "userDeviceName":
|
||||
name = prop["value"]
|
||||
_LOGGER.debug("Device %s is connected", mac)
|
||||
self.last_results[mac] = name
|
||||
except (KeyError, IndexError):
|
||||
_LOGGER.exception("Router returned unexpected response")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _make_request(self):
|
||||
# Weirdly enough, this doesn't seem to require authentication
|
||||
data = [{
|
||||
"request": {
|
||||
"sinceRevision": 0
|
||||
},
|
||||
"action": "http://linksys.com/jnap/devicelist/GetDevices"
|
||||
}]
|
||||
headers = {"X-JNAP-Action": "http://linksys.com/jnap/core/Transaction"}
|
||||
return requests.post('http://{}/JNAP/'.format(self.host),
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
headers=headers,
|
||||
json=data)
|
||||
@@ -158,6 +158,11 @@ class MikrotikScanner(DeviceScanner):
|
||||
for device in devices
|
||||
}
|
||||
else:
|
||||
self.last_results = mac_names
|
||||
self.last_results = {
|
||||
device.get('mac-address'):
|
||||
mac_names.get(device.get('mac-address'))
|
||||
for device in device_names
|
||||
if device.get('active-address')
|
||||
}
|
||||
|
||||
return True
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.components import zone as zone_comp
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
REQUIREMENTS = ['libnacl==1.5.0']
|
||||
REQUIREMENTS = ['libnacl==1.5.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -116,7 +116,6 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"key for topic %s", topic)
|
||||
return None
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def validate_payload(topic, payload, data_type):
|
||||
"""Validate the OwnTracks payload."""
|
||||
try:
|
||||
|
||||
@@ -57,7 +57,7 @@ class Host(object):
|
||||
def update(self, see):
|
||||
"""Update device state by sending one or more ping messages."""
|
||||
failed = 0
|
||||
while failed < self._count: # check more times if host in unreachable
|
||||
while failed < self._count: # check more times if host is unreachable
|
||||
if self.ping():
|
||||
see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
|
||||
return True
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.3.7']
|
||||
REQUIREMENTS = ['pysnmp==4.3.8']
|
||||
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
|
||||
32
homeassistant/components/device_tracker/ubus.py
Executable file → Normal file
32
homeassistant/components/device_tracker/ubus.py
Executable file → Normal file
@@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
@@ -38,6 +39,23 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
def _refresh_on_acccess_denied(func):
|
||||
"""If remove rebooted, it lost our session so rebuld one and try again."""
|
||||
def decorator(self, *args, **kwargs):
|
||||
"""Wrapper function to refresh session_id on PermissionError."""
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except PermissionError:
|
||||
_LOGGER.warning("Invalid session detected." +
|
||||
" Tryign to refresh session_id and re-run the rpc")
|
||||
self.session_id = _get_session_id(self.url, self.username,
|
||||
self.password)
|
||||
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class UbusDeviceScanner(DeviceScanner):
|
||||
"""
|
||||
This class queries a wireless router running OpenWrt firmware.
|
||||
@@ -48,14 +66,16 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
self.lock = threading.Lock()
|
||||
self.last_results = {}
|
||||
self.url = 'http://{}/ubus'.format(host)
|
||||
|
||||
self.session_id = _get_session_id(self.url, username, password)
|
||||
self.session_id = _get_session_id(self.url, self.username,
|
||||
self.password)
|
||||
self.hostapd = []
|
||||
self.leasefile = None
|
||||
self.mac2name = None
|
||||
@@ -66,6 +86,7 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
self._update_info()
|
||||
return self.last_results
|
||||
|
||||
@_refresh_on_acccess_denied
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
@@ -95,6 +116,7 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
return self.mac2name.get(device.upper(), None)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
@_refresh_on_acccess_denied
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the Luci router is up to date.
|
||||
|
||||
@@ -142,6 +164,12 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
|
||||
|
||||
if res.status_code == 200:
|
||||
response = res.json()
|
||||
if 'error' in response:
|
||||
if 'message' in response['error'] and \
|
||||
response['error']['message'] == "Access denied":
|
||||
raise PermissionError(response['error']['message'])
|
||||
else:
|
||||
raise HomeAssistantError(response['error']['message'])
|
||||
|
||||
if rpcmethod == "call":
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.unifi/
|
||||
"""
|
||||
import logging
|
||||
import urllib
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -15,7 +14,7 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import CONF_VERIFY_SSL
|
||||
|
||||
REQUIREMENTS = ['pyunifi==2.12']
|
||||
REQUIREMENTS = ['pyunifi==2.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
@@ -40,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Set up the Unifi device_tracker."""
|
||||
from pyunifi.controller import Controller
|
||||
from pyunifi.controller import Controller, APIError
|
||||
|
||||
host = config[DOMAIN].get(CONF_HOST)
|
||||
username = config[DOMAIN].get(CONF_USERNAME)
|
||||
@@ -53,7 +52,7 @@ def get_scanner(hass, config):
|
||||
try:
|
||||
ctrl = Controller(host, username, password, port, version='v4',
|
||||
site_id=site_id, ssl_verify=verify_ssl)
|
||||
except urllib.error.HTTPError as ex:
|
||||
except APIError as ex:
|
||||
_LOGGER.error("Failed to connect to Unifi: %s", ex)
|
||||
persistent_notification.create(
|
||||
hass, 'Failed to connect to Unifi. '
|
||||
@@ -77,9 +76,10 @@ class UnifiScanner(DeviceScanner):
|
||||
|
||||
def _update(self):
|
||||
"""Get the clients from the device."""
|
||||
from pyunifi.controller import APIError
|
||||
try:
|
||||
clients = self._controller.get_clients()
|
||||
except urllib.error.HTTPError as ex:
|
||||
except APIError as ex:
|
||||
_LOGGER.error("Failed to scan clients: %s", ex)
|
||||
clients = []
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ https://home-assistant.io/components/device_tracker.volvooncall/
|
||||
import logging
|
||||
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.components.volvooncall import DOMAIN
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
dispatcher_connect, dispatcher_send)
|
||||
from homeassistant.components.volvooncall import (
|
||||
DATA_KEY, SIGNAL_VEHICLE_SEEN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,19 +21,19 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
return
|
||||
|
||||
vin, _ = discovery_info
|
||||
vehicle = hass.data[DOMAIN].vehicles[vin]
|
||||
|
||||
host_name = vehicle.registration_number
|
||||
dev_id = 'volvo_' + slugify(host_name)
|
||||
vehicle = hass.data[DATA_KEY].vehicles[vin]
|
||||
|
||||
def see_vehicle(vehicle):
|
||||
"""Handle the reporting of the vehicle position."""
|
||||
host_name = vehicle.registration_number
|
||||
dev_id = 'volvo_{}'.format(slugify(host_name))
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=(vehicle.position['latitude'],
|
||||
vehicle.position['longitude']))
|
||||
vehicle.position['longitude']),
|
||||
icon='mdi:car')
|
||||
|
||||
hass.data[DOMAIN].entities[vin].append(see_vehicle)
|
||||
see_vehicle(vehicle)
|
||||
dispatcher_connect(hass, SIGNAL_VEHICLE_SEEN, see_vehicle)
|
||||
dispatcher_send(hass, SIGNAL_VEHICLE_SEEN, vehicle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-digitalocean==1.11']
|
||||
REQUIREMENTS = ['python-digitalocean==1.12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,7 +29,7 @@ ATTR_VCPUS = 'vcpus'
|
||||
|
||||
CONF_DROPLETS = 'droplets'
|
||||
|
||||
DIGITAL_OCEAN = None
|
||||
DATA_DIGITAL_OCEAN = 'data_do'
|
||||
DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor']
|
||||
DOMAIN = 'digital_ocean'
|
||||
|
||||
@@ -47,13 +47,14 @@ def setup(hass, config):
|
||||
conf = config[DOMAIN]
|
||||
access_token = conf.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
global DIGITAL_OCEAN
|
||||
DIGITAL_OCEAN = DigitalOcean(access_token)
|
||||
digital = DigitalOcean(access_token)
|
||||
|
||||
if not DIGITAL_OCEAN.manager.get_account():
|
||||
if not digital.manager.get_account():
|
||||
_LOGGER.error("No Digital Ocean account found for the given API Token")
|
||||
return False
|
||||
|
||||
hass.data[DATA_DIGITAL_OCEAN] = digital
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ SERVICE_HANDLERS = {
|
||||
'apple_tv': ('media_player', 'apple_tv'),
|
||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
'harmony': ('remote', 'harmony'),
|
||||
'bose_soundtouch': ('media_player', 'soundtouch'),
|
||||
}
|
||||
|
||||
|
||||
98
homeassistant/components/dyson.py
Normal file
98
homeassistant/components/dyson.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Parent component for Dyson Pure Cool Link devices."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \
|
||||
CONF_DEVICES
|
||||
|
||||
REQUIREMENTS = ['libpurecoollink==0.1.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_LANGUAGE = "language"
|
||||
CONF_RETRY = "retry"
|
||||
|
||||
DEFAULT_TIMEOUT = 5
|
||||
DEFAULT_RETRY = 10
|
||||
|
||||
DOMAIN = "dyson"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_LANGUAGE): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int,
|
||||
vol.Optional(CONF_DEVICES, default=[]):
|
||||
vol.All(cv.ensure_list, [dict]),
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
DYSON_DEVICES = "dyson_devices"
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Dyson parent component."""
|
||||
_LOGGER.info("Creating new Dyson component")
|
||||
|
||||
if DYSON_DEVICES not in hass.data:
|
||||
hass.data[DYSON_DEVICES] = []
|
||||
|
||||
from libpurecoollink.dyson import DysonAccount
|
||||
dyson_account = DysonAccount(config[DOMAIN].get(CONF_USERNAME),
|
||||
config[DOMAIN].get(CONF_PASSWORD),
|
||||
config[DOMAIN].get(CONF_LANGUAGE))
|
||||
|
||||
logged = dyson_account.login()
|
||||
|
||||
timeout = config[DOMAIN].get(CONF_TIMEOUT)
|
||||
retry = config[DOMAIN].get(CONF_RETRY)
|
||||
|
||||
if not logged:
|
||||
_LOGGER.error("Not connected to Dyson account. Unable to add devices")
|
||||
return False
|
||||
|
||||
_LOGGER.info("Connected to Dyson account")
|
||||
dyson_devices = dyson_account.devices()
|
||||
if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES):
|
||||
configured_devices = config[DOMAIN].get(CONF_DEVICES)
|
||||
for device in configured_devices:
|
||||
dyson_device = next((d for d in dyson_devices if
|
||||
d.serial == device["device_id"]), None)
|
||||
if dyson_device:
|
||||
connected = dyson_device.connect(None, device["device_ip"],
|
||||
timeout, retry)
|
||||
if connected:
|
||||
_LOGGER.info("Connected to device %s", dyson_device)
|
||||
hass.data[DYSON_DEVICES].append(dyson_device)
|
||||
else:
|
||||
_LOGGER.warning("Unable to connect to device %s",
|
||||
dyson_device)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Unable to find device %s in Dyson account",
|
||||
device["device_id"])
|
||||
else:
|
||||
# Not yet reliable
|
||||
for device in dyson_devices:
|
||||
_LOGGER.info("Trying to connect to device %s with timeout=%i "
|
||||
"and retry=%i", device, timeout, retry)
|
||||
connected = device.connect(None, None, timeout, retry)
|
||||
if connected:
|
||||
_LOGGER.info("Connected to device %s", device)
|
||||
hass.data[DYSON_DEVICES].append(device)
|
||||
else:
|
||||
_LOGGER.warning("Unable to connect to device %s", device)
|
||||
|
||||
# Start fan/sensors components
|
||||
if hass.data[DYSON_DEVICES]:
|
||||
_LOGGER.debug("Starting sensor/fan components")
|
||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "fan", DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
@@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
REQUIREMENTS = ['pyeight==0.0.6']
|
||||
REQUIREMENTS = ['pyeight==0.0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'fan'
|
||||
|
||||
DEPENDENCIES = ['group']
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
GROUP_NAME_ALL_FANS = 'all fans'
|
||||
@@ -73,7 +73,7 @@ FAN_TURN_ON_SCHEMA = vol.Schema({
|
||||
}) # type: dict
|
||||
|
||||
FAN_TURN_OFF_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids
|
||||
}) # type: dict
|
||||
|
||||
FAN_OSCILLATE_SCHEMA = vol.Schema({
|
||||
@@ -139,9 +139,7 @@ def turn_on(hass, entity_id: str=None, speed: str=None) -> None:
|
||||
|
||||
def turn_off(hass, entity_id: str=None) -> None:
|
||||
"""Turn all or specified fan off."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
}
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
@@ -218,8 +216,7 @@ def async_setup(hass, config: dict):
|
||||
if not fan.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.async_add_job(
|
||||
fan.async_update_ha_state(True))
|
||||
update_coro = hass.async_add_job(fan.async_update_ha_state(True))
|
||||
if hasattr(fan, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
|
||||
118
homeassistant/components/fan/comfoconnect.py
Normal file
118
homeassistant/components/fan/comfoconnect.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.comfoconnect/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.comfoconnect import (
|
||||
DOMAIN, ComfoConnectBridge, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED)
|
||||
from homeassistant.components.fan import (
|
||||
FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.helpers.dispatcher import (dispatcher_connect)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['comfoconnect']
|
||||
|
||||
SPEED_MAPPING = {
|
||||
0: SPEED_OFF,
|
||||
1: SPEED_LOW,
|
||||
2: SPEED_MEDIUM,
|
||||
3: SPEED_HIGH
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the ComfoConnect fan platform."""
|
||||
ccb = hass.data[DOMAIN]
|
||||
|
||||
add_devices([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True)
|
||||
return
|
||||
|
||||
|
||||
class ComfoConnectFan(FanEntity):
|
||||
"""Representation of the ComfoConnect fan platform."""
|
||||
|
||||
def __init__(self, hass, name, ccb: ComfoConnectBridge):
|
||||
"""Initialize the ComfoConnect fan."""
|
||||
from pycomfoconnect import SENSOR_FAN_SPEED_MODE
|
||||
|
||||
self._ccb = ccb
|
||||
self._name = name
|
||||
|
||||
# Ask the bridge to keep us updated
|
||||
self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE)
|
||||
|
||||
def _handle_update(var):
|
||||
if var == SENSOR_FAN_SPEED_MODE:
|
||||
_LOGGER.debug("Dispatcher update for %s", var)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
# Register for dispatcher updates
|
||||
dispatcher_connect(
|
||||
hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the fan."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return 'mdi:air-conditioner'
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_SET_SPEED
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
"""Return the current fan mode."""
|
||||
from pycomfoconnect import (SENSOR_FAN_SPEED_MODE)
|
||||
|
||||
try:
|
||||
speed = self._ccb.data[SENSOR_FAN_SPEED_MODE]
|
||||
return SPEED_MAPPING[speed]
|
||||
except KeyError:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def speed_list(self):
|
||||
"""List of available fan modes."""
|
||||
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
def turn_on(self, speed: str=None, **kwargs) -> None:
|
||||
"""Turn on the fan."""
|
||||
if speed is None:
|
||||
speed = SPEED_LOW
|
||||
self.set_speed(speed)
|
||||
|
||||
def turn_off(self) -> None:
|
||||
"""Turn off the fan (to away)."""
|
||||
self.set_speed(SPEED_OFF)
|
||||
|
||||
def set_speed(self, mode):
|
||||
"""Set fan speed."""
|
||||
_LOGGER.debug('Changing fan mode to %s.', mode)
|
||||
|
||||
from pycomfoconnect import (
|
||||
CMD_FAN_MODE_AWAY, CMD_FAN_MODE_LOW, CMD_FAN_MODE_MEDIUM,
|
||||
CMD_FAN_MODE_HIGH)
|
||||
|
||||
if mode == SPEED_OFF:
|
||||
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY)
|
||||
elif mode == SPEED_LOW:
|
||||
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW)
|
||||
elif mode == SPEED_MEDIUM:
|
||||
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM)
|
||||
elif mode == SPEED_HIGH:
|
||||
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH)
|
||||
|
||||
# Update current mode
|
||||
self.schedule_update_ha_state()
|
||||
@@ -9,31 +9,36 @@ from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_OSCILLATE, SUPPORT_DIRECTION)
|
||||
from homeassistant.const import STATE_OFF
|
||||
|
||||
FAN_NAME = 'Living Room Fan'
|
||||
FAN_ENTITY_ID = 'fan.living_room_fan'
|
||||
|
||||
DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
|
||||
FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
|
||||
LIMITED_SUPPORT = SUPPORT_SET_SPEED
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up the demo fan platform."""
|
||||
add_devices_callback([
|
||||
DemoFan(hass, FAN_NAME, STATE_OFF),
|
||||
DemoFan(hass, "Living Room Fan", FULL_SUPPORT),
|
||||
DemoFan(hass, "Ceiling Fan", LIMITED_SUPPORT),
|
||||
])
|
||||
|
||||
|
||||
class DemoFan(FanEntity):
|
||||
"""A demonstration fan component."""
|
||||
|
||||
def __init__(self, hass, name: str, initial_state: str) -> None:
|
||||
def __init__(self, hass, name: str, supported_features: int) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.hass = hass
|
||||
self._speed = initial_state
|
||||
self.oscillating = False
|
||||
self.direction = "forward"
|
||||
self._supported_features = supported_features
|
||||
self._speed = STATE_OFF
|
||||
self.oscillating = None
|
||||
self.direction = None
|
||||
self._name = name
|
||||
|
||||
if supported_features & SUPPORT_OSCILLATE:
|
||||
self.oscillating = False
|
||||
if supported_features & SUPPORT_DIRECTION:
|
||||
self.direction = "forward"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get entity name."""
|
||||
@@ -88,4 +93,4 @@ class DemoFan(FanEntity):
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return DEMO_SUPPORT
|
||||
return self._supported_features
|
||||
|
||||
218
homeassistant/components/fan/dyson.py
Normal file
218
homeassistant/components/fan/dyson.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""Support for Dyson Pure Cool link fan."""
|
||||
import logging
|
||||
import asyncio
|
||||
from os import path
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE,
|
||||
SUPPORT_SET_SPEED,
|
||||
DOMAIN)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.dyson import DYSON_DEVICES
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
DEPENDENCIES = ['dyson']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DYSON_FAN_DEVICES = "dyson_fan_devices"
|
||||
SERVICE_SET_NIGHT_MODE = 'dyson_set_night_mode'
|
||||
|
||||
DYSON_SET_NIGHT_MODE_SCHEMA = vol.Schema({
|
||||
vol.Required('entity_id'): cv.entity_id,
|
||||
vol.Required('night_mode'): cv.boolean
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Dyson fan components."""
|
||||
_LOGGER.info("Creating new Dyson fans")
|
||||
if DYSON_FAN_DEVICES not in hass.data:
|
||||
hass.data[DYSON_FAN_DEVICES] = []
|
||||
|
||||
# Get Dyson Devices from parent component
|
||||
for device in hass.data[DYSON_DEVICES]:
|
||||
dyson_entity = DysonPureCoolLinkDevice(hass, device)
|
||||
hass.data[DYSON_FAN_DEVICES].append(dyson_entity)
|
||||
|
||||
add_devices(hass.data[DYSON_FAN_DEVICES])
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def service_handle(service):
|
||||
"""Handle dyson services."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
night_mode = service.data.get('night_mode')
|
||||
fan_device = next([fan for fan in hass.data[DYSON_FAN_DEVICES] if
|
||||
fan.entity_id == entity_id].__iter__(), None)
|
||||
if fan_device is None:
|
||||
_LOGGER.warning("Unable to find Dyson fan device %s",
|
||||
str(entity_id))
|
||||
return
|
||||
|
||||
if service.service == SERVICE_SET_NIGHT_MODE:
|
||||
fan_device.night_mode(night_mode)
|
||||
|
||||
# Register dyson service(s)
|
||||
hass.services.register(DOMAIN, SERVICE_SET_NIGHT_MODE,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_SET_NIGHT_MODE),
|
||||
schema=DYSON_SET_NIGHT_MODE_SCHEMA)
|
||||
|
||||
|
||||
class DysonPureCoolLinkDevice(FanEntity):
|
||||
"""Representation of a Dyson fan."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize the fan."""
|
||||
_LOGGER.info("Creating device %s", device.name)
|
||||
self.hass = hass
|
||||
self._device = device
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.async_add_job(
|
||||
self._device.add_message_listener, self.on_message)
|
||||
|
||||
def on_message(self, message):
|
||||
"""Called when new messages received from the fan."""
|
||||
_LOGGER.debug(
|
||||
"Message received for fan device %s : %s", self.name, message)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this fan."""
|
||||
return self._device.name
|
||||
|
||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan. Never called ??."""
|
||||
_LOGGER.debug("Set fan speed to: " + speed)
|
||||
from libpurecoollink.const import FanSpeed, FanMode
|
||||
if speed == FanSpeed.FAN_SPEED_AUTO.value:
|
||||
self._device.set_configuration(fan_mode=FanMode.AUTO)
|
||||
else:
|
||||
fan_speed = FanSpeed('{0:04d}'.format(int(speed)))
|
||||
self._device.set_configuration(fan_mode=FanMode.FAN,
|
||||
fan_speed=fan_speed)
|
||||
|
||||
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
||||
"""Turn on the fan."""
|
||||
_LOGGER.debug("Turn on fan %s with speed %s", self.name, speed)
|
||||
from libpurecoollink.const import FanSpeed, FanMode
|
||||
if speed:
|
||||
if speed == FanSpeed.FAN_SPEED_AUTO.value:
|
||||
self._device.set_configuration(fan_mode=FanMode.AUTO)
|
||||
else:
|
||||
fan_speed = FanSpeed('{0:04d}'.format(int(speed)))
|
||||
self._device.set_configuration(fan_mode=FanMode.FAN,
|
||||
fan_speed=fan_speed)
|
||||
else:
|
||||
# Speed not set, just turn on
|
||||
self._device.set_configuration(fan_mode=FanMode.FAN)
|
||||
|
||||
def turn_off(self: ToggleEntity, **kwargs) -> None:
|
||||
"""Turn off the fan."""
|
||||
_LOGGER.debug("Turn off fan %s", self.name)
|
||||
from libpurecoollink.const import FanMode
|
||||
self._device.set_configuration(fan_mode=FanMode.OFF)
|
||||
|
||||
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
|
||||
"""Turn on/off oscillating."""
|
||||
_LOGGER.debug("Turn oscillation %s for device %s", oscillating,
|
||||
self.name)
|
||||
from libpurecoollink.const import Oscillation
|
||||
|
||||
if oscillating:
|
||||
self._device.set_configuration(
|
||||
oscillation=Oscillation.OSCILLATION_ON)
|
||||
else:
|
||||
self._device.set_configuration(
|
||||
oscillation=Oscillation.OSCILLATION_OFF)
|
||||
|
||||
@property
|
||||
def oscillating(self):
|
||||
"""Return the oscillation state."""
|
||||
return self._device.state and self._device.state.oscillation == "ON"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the entity is on."""
|
||||
if self._device.state:
|
||||
return self._device.state.fan_state == "FAN"
|
||||
return False
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
if self._device.state:
|
||||
from libpurecoollink.const import FanSpeed
|
||||
if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value:
|
||||
return self._device.state.speed
|
||||
else:
|
||||
return int(self._device.state.speed)
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_direction(self):
|
||||
"""Return direction of the fan [forward, reverse]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_night_mode(self):
|
||||
"""Return Night mode."""
|
||||
return self._device.state.night_mode == "ON"
|
||||
|
||||
def night_mode(self: ToggleEntity, night_mode: bool) -> None:
|
||||
"""Turn fan in night mode."""
|
||||
_LOGGER.debug("Set %s night mode %s", self.name, night_mode)
|
||||
from libpurecoollink.const import NightMode
|
||||
if night_mode:
|
||||
self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON)
|
||||
else:
|
||||
self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_OFF)
|
||||
|
||||
@property
|
||||
def is_auto_mode(self):
|
||||
"""Return auto mode."""
|
||||
return self._device.state.fan_mode == "AUTO"
|
||||
|
||||
def auto_mode(self: ToggleEntity, auto_mode: bool) -> None:
|
||||
"""Turn fan in auto mode."""
|
||||
_LOGGER.debug("Set %s auto mode %s", self.name, auto_mode)
|
||||
from libpurecoollink.const import FanMode
|
||||
if auto_mode:
|
||||
self._device.set_configuration(fan_mode=FanMode.AUTO)
|
||||
else:
|
||||
self._device.set_configuration(fan_mode=FanMode.FAN)
|
||||
|
||||
@property
|
||||
def speed_list(self: ToggleEntity) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
from libpurecoollink.const import FanSpeed
|
||||
supported_speeds = [FanSpeed.FAN_SPEED_AUTO.value,
|
||||
int(FanSpeed.FAN_SPEED_1.value),
|
||||
int(FanSpeed.FAN_SPEED_2.value),
|
||||
int(FanSpeed.FAN_SPEED_3.value),
|
||||
int(FanSpeed.FAN_SPEED_4.value),
|
||||
int(FanSpeed.FAN_SPEED_5.value),
|
||||
int(FanSpeed.FAN_SPEED_6.value),
|
||||
int(FanSpeed.FAN_SPEED_7.value),
|
||||
int(FanSpeed.FAN_SPEED_8.value),
|
||||
int(FanSpeed.FAN_SPEED_9.value),
|
||||
int(FanSpeed.FAN_SPEED_10.value)]
|
||||
|
||||
return supported_speeds
|
||||
|
||||
@property
|
||||
def supported_features(self: ToggleEntity) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED
|
||||
195
homeassistant/components/fan/insteon_local.py
Normal file
195
homeassistant/components/fan/insteon_local.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Support for Insteon fans via local hub control.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.insteon_local/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
ATTR_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED, FanEntity)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['insteon_local']
|
||||
DOMAIN = 'fan'
|
||||
|
||||
INSTEON_LOCAL_FANS_CONF = 'insteon_local_fans.conf'
|
||||
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
SUPPORT_INSTEON_LOCAL = SUPPORT_SET_SPEED
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Insteon local fan platform."""
|
||||
insteonhub = hass.data['insteon_local']
|
||||
|
||||
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
|
||||
if len(conf_fans):
|
||||
for device_id in conf_fans:
|
||||
setup_fan(device_id, conf_fans[device_id], insteonhub, hass,
|
||||
add_devices)
|
||||
|
||||
else:
|
||||
linked = insteonhub.get_linked()
|
||||
|
||||
for device_id in linked:
|
||||
if (linked[device_id]['cat_type'] == 'dimmer' and
|
||||
linked[device_id]['sku'] == '2475F' and
|
||||
device_id not in conf_fans):
|
||||
request_configuration(device_id,
|
||||
insteonhub,
|
||||
linked[device_id]['model_name'] + ' ' +
|
||||
linked[device_id]['sku'],
|
||||
hass, add_devices)
|
||||
|
||||
|
||||
def request_configuration(device_id, insteonhub, model, hass,
|
||||
add_devices_callback):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = get_component('configurator')
|
||||
|
||||
# We got an error if this method is called while we are configuring
|
||||
if device_id in _CONFIGURING:
|
||||
configurator.notify_errors(
|
||||
_CONFIGURING[device_id], 'Failed to register, please try again.')
|
||||
|
||||
return
|
||||
|
||||
def insteon_fan_config_callback(data):
|
||||
"""The actions to do when our configuration callback is called."""
|
||||
setup_fan(device_id, data.get('name'), insteonhub, hass,
|
||||
add_devices_callback)
|
||||
|
||||
_CONFIGURING[device_id] = configurator.request_config(
|
||||
hass, 'Insteon ' + model + ' addr: ' + device_id,
|
||||
insteon_fan_config_callback,
|
||||
description=('Enter a name for ' + model + ' Fan addr: ' + device_id),
|
||||
entity_picture='/static/images/config_insteon.png',
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': 'name', 'name': 'Name', 'type': ''}]
|
||||
)
|
||||
|
||||
|
||||
def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
|
||||
"""Set up the fan."""
|
||||
if device_id in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(device_id)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
_LOGGER.info("Device configuration done!")
|
||||
|
||||
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
|
||||
if device_id not in conf_fans:
|
||||
conf_fans[device_id] = name
|
||||
|
||||
if not config_from_file(
|
||||
hass.config.path(INSTEON_LOCAL_FANS_CONF),
|
||||
conf_fans):
|
||||
_LOGGER.error("Failed to save configuration file")
|
||||
|
||||
device = insteonhub.fan(device_id)
|
||||
add_devices_callback([InsteonLocalFanDevice(device, name)])
|
||||
|
||||
|
||||
def config_from_file(filename, config=None):
|
||||
"""Small configuration file management function."""
|
||||
if config:
|
||||
# We're writing configuration
|
||||
try:
|
||||
with open(filename, 'w') as fdesc:
|
||||
fdesc.write(json.dumps(config))
|
||||
except IOError as error:
|
||||
_LOGGER.error('Saving config file failed: %s', error)
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
# We're reading config
|
||||
if os.path.isfile(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fdesc:
|
||||
return json.loads(fdesc.read())
|
||||
except IOError as error:
|
||||
_LOGGER.error("Reading configuration file failed: %s", error)
|
||||
# This won't work yet
|
||||
return False
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
class InsteonLocalFanDevice(FanEntity):
|
||||
"""An abstract Class for an Insteon node."""
|
||||
|
||||
def __init__(self, node, name):
|
||||
"""Initialize the device."""
|
||||
self.node = node
|
||||
self.node.deviceName = name
|
||||
self._speed = SPEED_OFF
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the the name of the node."""
|
||||
return self.node.deviceName
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this Insteon node."""
|
||||
return 'insteon_local_{}_fan'.format(self.node.device_id)
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return self._speed
|
||||
|
||||
@property
|
||||
def speed_list(self: ToggleEntity) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update(self):
|
||||
"""Update state of the fan."""
|
||||
resp = self.node.status()
|
||||
if 'cmd2' in resp:
|
||||
if resp['cmd2'] == '00':
|
||||
self._speed = SPEED_OFF
|
||||
elif resp['cmd2'] == '55':
|
||||
self._speed = SPEED_LOW
|
||||
elif resp['cmd2'] == 'AA':
|
||||
self._speed = SPEED_MEDIUM
|
||||
elif resp['cmd2'] == 'FF':
|
||||
self._speed = SPEED_HIGH
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_INSTEON_LOCAL
|
||||
|
||||
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
||||
"""Turn device on."""
|
||||
if speed is None:
|
||||
if ATTR_SPEED in kwargs:
|
||||
speed = kwargs[ATTR_SPEED]
|
||||
else:
|
||||
speed = SPEED_MEDIUM
|
||||
|
||||
self.set_speed(speed)
|
||||
|
||||
def turn_off(self: ToggleEntity, **kwargs) -> None:
|
||||
"""Turn device off."""
|
||||
self.node.off()
|
||||
|
||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
if self.node.on(speed):
|
||||
self._speed = speed
|
||||
@@ -58,7 +58,18 @@ set_direction:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of the entities to toggle
|
||||
exampl: 'fan.living_room'
|
||||
example: 'fan.living_room'
|
||||
direction:
|
||||
description: The direction to rotate
|
||||
example: 'left'
|
||||
|
||||
dyson_set_night_mode:
|
||||
description: Set the fan in night mode
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of the entities to enable/disable night mode
|
||||
example: 'fan.living_room'
|
||||
night_mode:
|
||||
description: Night mode status
|
||||
example: true
|
||||
|
||||
@@ -36,7 +36,7 @@ SPEED_TO_VALUE = {
|
||||
|
||||
|
||||
def get_device(values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
"""Create Z-Wave entity device."""
|
||||
return ZwaveFan(values)
|
||||
|
||||
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
|
||||
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
|
||||
"frontend.html": "ed18c05632c071eb4f7b012382d0f810",
|
||||
"mdi.html": "f407a5a57addbe93817ee1b244d33fbe",
|
||||
"frontend.html": "f170a7221615ca2839cb8fd51a82f50a",
|
||||
"mdi.html": "c92bd28c434865d6cabb34cd3c0a3e4c",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
|
||||
"panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1",
|
||||
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2",
|
||||
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
|
||||
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed",
|
||||
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750",
|
||||
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b",
|
||||
"panels/ha-panel-automation.html": "4f98839bb082885657bbcd0ac04fc680",
|
||||
"panels/ha-panel-config.html": "76853de505d173e82249bf605eb73505",
|
||||
"panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61",
|
||||
"panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc",
|
||||
"panels/ha-panel-dev-service.html": "92c6be30b1af95791d5a6429df505852",
|
||||
"panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869",
|
||||
"panels/ha-panel-dev-template.html": "d33a55b937b50cdfe8b6fae81f70a139",
|
||||
"panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229",
|
||||
"panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1",
|
||||
"panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
|
||||
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
|
||||
"panels/ha-panel-zwave.html": "780a792213e98510b475f752c40ef0f9",
|
||||
"panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d",
|
||||
"panels/ha-panel-map.html": "0ba605729197c4724ecc7310b08f7050",
|
||||
"panels/ha-panel-zwave.html": "2ea2223339d1d2faff478751c2927d11",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +1 @@
|
||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hass.config.core.version]]</p><p>Path to configuration.yaml: [[hass.config.core.config_dir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the Apache 2.0 license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a> — <a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},polymerVersion:{type:String,value:Polymer.version},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.callApi("GET","error_log").then(function(e){this.errorLog=e||"No errors have been reported."}.bind(this))}})</script></body></html>
|
||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header slot="header" fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hass.config.core.version]]</p><p>Path to configuration.yaml: [[hass.config.core.config_dir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the Apache 2.0 license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a> — <a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},polymerVersion:{type:String,value:Polymer.version},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.callApi("GET","error_log").then(function(e){this.errorLog=e||"No errors have been reported."}.bind(this))}})</script></body></html>
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -14,7 +14,8 @@ from homeassistant import config as conf_util, core as ha
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
|
||||
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE, SERVICE_RELOAD)
|
||||
STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN,
|
||||
ATTR_ASSUMED_STATE, SERVICE_RELOAD)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -30,13 +31,23 @@ CONF_ENTITIES = 'entities'
|
||||
CONF_VIEW = 'view'
|
||||
CONF_CONTROL = 'control'
|
||||
|
||||
ATTR_ADD_ENTITIES = 'add_entities'
|
||||
ATTR_AUTO = 'auto'
|
||||
ATTR_CONTROL = 'control'
|
||||
ATTR_ENTITIES = 'entities'
|
||||
ATTR_ICON = 'icon'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_OBJECT_ID = 'object_id'
|
||||
ATTR_ORDER = 'order'
|
||||
ATTR_VIEW = 'view'
|
||||
ATTR_VISIBLE = 'visible'
|
||||
ATTR_CONTROL = 'control'
|
||||
|
||||
SERVICE_SET_VISIBILITY = 'set_visibility'
|
||||
SERVICE_SET = 'set'
|
||||
SERVICE_REMOVE = 'remove'
|
||||
|
||||
CONTROL_TYPES = vol.In(['hidden', None])
|
||||
|
||||
SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_VISIBLE): cv.boolean
|
||||
@@ -44,6 +55,21 @@ SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({
|
||||
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
SET_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_OBJECT_ID): cv.slug,
|
||||
vol.Optional(ATTR_NAME): cv.string,
|
||||
vol.Optional(ATTR_VIEW): cv.boolean,
|
||||
vol.Optional(ATTR_ICON): cv.string,
|
||||
vol.Optional(ATTR_CONTROL): CONTROL_TYPES,
|
||||
vol.Optional(ATTR_VISIBLE): cv.boolean,
|
||||
vol.Exclusive(ATTR_ENTITIES, 'entities'): cv.entity_ids,
|
||||
vol.Exclusive(ATTR_ADD_ENTITIES, 'entities'): cv.entity_ids,
|
||||
})
|
||||
|
||||
REMOVE_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_OBJECT_ID): cv.slug,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -60,7 +86,7 @@ GROUP_SCHEMA = vol.Schema({
|
||||
CONF_VIEW: cv.boolean,
|
||||
CONF_NAME: cv.string,
|
||||
CONF_ICON: cv.icon,
|
||||
CONF_CONTROL: cv.string,
|
||||
CONF_CONTROL: CONTROL_TYPES,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@@ -69,7 +95,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
# List of ON/OFF state tuples for groupable states
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
|
||||
(STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED)]
|
||||
(STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED),
|
||||
(STATE_PROBLEM, STATE_OK)]
|
||||
|
||||
|
||||
def _get_group_on_off(state):
|
||||
@@ -99,10 +126,10 @@ def reload(hass):
|
||||
hass.add_job(async_reload, hass)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def async_reload(hass):
|
||||
"""Reload the automation from config."""
|
||||
yield from hass.services.async_call(DOMAIN, SERVICE_RELOAD)
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RELOAD))
|
||||
|
||||
|
||||
def set_visibility(hass, entity_id=None, visible=True):
|
||||
@@ -111,6 +138,46 @@ def set_visibility(hass, entity_id=None, visible=True):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_VISIBILITY, data)
|
||||
|
||||
|
||||
def set_group(hass, object_id, name=None, entity_ids=None, visible=None,
|
||||
icon=None, view=None, control=None, add=None):
|
||||
"""Create a new user group."""
|
||||
hass.add_job(
|
||||
async_set_group, hass, object_id, name, entity_ids, visible, icon,
|
||||
view, control, add)
|
||||
|
||||
|
||||
@callback
|
||||
def async_set_group(hass, object_id, name=None, entity_ids=None, visible=None,
|
||||
icon=None, view=None, control=None, add=None):
|
||||
"""Create a new user group."""
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
(ATTR_OBJECT_ID, object_id),
|
||||
(ATTR_NAME, name),
|
||||
(ATTR_ENTITIES, entity_ids),
|
||||
(ATTR_VISIBLE, visible),
|
||||
(ATTR_ICON, icon),
|
||||
(ATTR_VIEW, view),
|
||||
(ATTR_CONTROL, control),
|
||||
(ATTR_ADD_ENTITIES, add),
|
||||
] if value is not None
|
||||
}
|
||||
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SET, data))
|
||||
|
||||
|
||||
def remove(hass, name):
|
||||
"""Remove a user group."""
|
||||
hass.add_job(async_remove, hass, name)
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove(hass, object_id):
|
||||
"""Remove a user group."""
|
||||
data = {ATTR_OBJECT_ID: object_id}
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_REMOVE, data))
|
||||
|
||||
|
||||
def expand_entity_ids(hass, entity_ids):
|
||||
"""Return entity_ids with group entity ids replaced by their members.
|
||||
|
||||
@@ -170,6 +237,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
|
||||
def async_setup(hass, config):
|
||||
"""Set up all groups found definded in the configuration."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
service_groups = {}
|
||||
|
||||
yield from _async_process_config(hass, config, component)
|
||||
|
||||
@@ -179,29 +247,116 @@ def async_setup(hass, config):
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
def reload_service_handler(service):
|
||||
"""Remove all groups and load new ones from config."""
|
||||
conf = yield from component.async_prepare_reload()
|
||||
if conf is None:
|
||||
return
|
||||
yield from _async_process_config(hass, conf, component)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_RELOAD], schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def groups_service_handler(service):
|
||||
"""Handle dynamic group service functions."""
|
||||
object_id = service.data[ATTR_OBJECT_ID]
|
||||
|
||||
# new group
|
||||
if service.service == SERVICE_SET and object_id not in service_groups:
|
||||
entity_ids = service.data.get(ATTR_ENTITIES) or \
|
||||
service.data.get(ATTR_ADD_ENTITIES) or None
|
||||
|
||||
extra_arg = {attr: service.data[attr] for attr in (
|
||||
ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL
|
||||
) if service.data.get(attr) is not None}
|
||||
|
||||
new_group = yield from Group.async_create_group(
|
||||
hass, service.data.get(ATTR_NAME, object_id),
|
||||
object_id=object_id,
|
||||
entity_ids=entity_ids,
|
||||
user_defined=False,
|
||||
**extra_arg
|
||||
)
|
||||
|
||||
service_groups[object_id] = new_group
|
||||
return
|
||||
|
||||
# update group
|
||||
if service.service == SERVICE_SET:
|
||||
group = service_groups[object_id]
|
||||
need_update = False
|
||||
|
||||
if ATTR_ADD_ENTITIES in service.data:
|
||||
delta = service.data[ATTR_ADD_ENTITIES]
|
||||
entity_ids = set(group.tracking) | set(delta)
|
||||
yield from group.async_update_tracked_entity_ids(entity_ids)
|
||||
|
||||
if ATTR_ENTITIES in service.data:
|
||||
entity_ids = service.data[ATTR_ENTITIES]
|
||||
yield from group.async_update_tracked_entity_ids(entity_ids)
|
||||
|
||||
if ATTR_NAME in service.data:
|
||||
group.name = service.data[ATTR_NAME]
|
||||
need_update = True
|
||||
|
||||
if ATTR_VISIBLE in service.data:
|
||||
group.visible = service.data[ATTR_VISIBLE]
|
||||
need_update = True
|
||||
|
||||
if ATTR_ICON in service.data:
|
||||
group.icon = service.data[ATTR_ICON]
|
||||
need_update = True
|
||||
|
||||
if ATTR_CONTROL in service.data:
|
||||
group.control = service.data[ATTR_CONTROL]
|
||||
need_update = True
|
||||
|
||||
if ATTR_VIEW in service.data:
|
||||
group.view = service.data[ATTR_VIEW]
|
||||
need_update = True
|
||||
|
||||
if need_update:
|
||||
yield from group.async_update_ha_state()
|
||||
|
||||
return
|
||||
|
||||
# remove group
|
||||
if service.service == SERVICE_REMOVE:
|
||||
if object_id not in service_groups:
|
||||
_LOGGER.warning("Group '%s' not exists!", object_id)
|
||||
return
|
||||
|
||||
del_group = service_groups.pop(object_id)
|
||||
yield from del_group.async_stop()
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET, groups_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_SET], schema=SET_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_REMOVE, groups_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_REMOVE], schema=REMOVE_SERVICE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def visibility_service_handler(service):
|
||||
"""Change visibility of a group."""
|
||||
visible = service.data.get(ATTR_VISIBLE)
|
||||
tasks = [group.async_set_visible(visible) for group
|
||||
in component.async_extract_from_service(service,
|
||||
expand_group=False)]
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
tasks = []
|
||||
for group in component.async_extract_from_service(service,
|
||||
expand_group=False):
|
||||
group.visible = visible
|
||||
tasks.append(group.async_update_ha_state())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_SET_VISIBILITY],
|
||||
schema=SET_VISIBILITY_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_RELOAD], schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -231,8 +386,8 @@ def _async_process_config(hass, config, component):
|
||||
class Group(Entity):
|
||||
"""Track a group of entity ids."""
|
||||
|
||||
def __init__(self, hass, name, order=None, user_defined=True, icon=None,
|
||||
view=False, control=None):
|
||||
def __init__(self, hass, name, order=None, visible=True, icon=None,
|
||||
view=False, control=None, user_defined=True):
|
||||
"""Initialize a group.
|
||||
|
||||
This Object has factory function for creation.
|
||||
@@ -240,31 +395,33 @@ class Group(Entity):
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
self._user_defined = user_defined
|
||||
self._order = order
|
||||
self._icon = icon
|
||||
self._view = view
|
||||
self.view = view
|
||||
self.tracking = []
|
||||
self.group_on = None
|
||||
self.group_off = None
|
||||
self.visible = visible
|
||||
self.control = control
|
||||
self._user_defined = user_defined
|
||||
self._order = order
|
||||
self._assumed_state = False
|
||||
self._async_unsub_state_changed = None
|
||||
self._visible = True
|
||||
self._control = control
|
||||
|
||||
@staticmethod
|
||||
def create_group(hass, name, entity_ids=None, user_defined=True,
|
||||
icon=None, view=False, control=None, object_id=None):
|
||||
visible=True, icon=None, view=False, control=None,
|
||||
object_id=None):
|
||||
"""Initialize a group."""
|
||||
return run_coroutine_threadsafe(
|
||||
Group.async_create_group(hass, name, entity_ids, user_defined,
|
||||
icon, view, control, object_id),
|
||||
Group.async_create_group(
|
||||
hass, name, entity_ids, user_defined, visible, icon, view,
|
||||
control, object_id),
|
||||
hass.loop).result()
|
||||
|
||||
@staticmethod
|
||||
@asyncio.coroutine
|
||||
def async_create_group(hass, name, entity_ids=None, user_defined=True,
|
||||
icon=None, view=False, control=None,
|
||||
visible=True, icon=None, view=False, control=None,
|
||||
object_id=None):
|
||||
"""Initialize a group.
|
||||
|
||||
@@ -273,8 +430,9 @@ class Group(Entity):
|
||||
group = Group(
|
||||
hass, name,
|
||||
order=len(hass.states.async_entity_ids(DOMAIN)),
|
||||
user_defined=user_defined, icon=icon, view=view,
|
||||
control=control)
|
||||
visible=visible, icon=icon, view=view, control=control,
|
||||
user_defined=user_defined
|
||||
)
|
||||
|
||||
group.entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, object_id or name, hass=hass)
|
||||
@@ -297,6 +455,11 @@ class Group(Entity):
|
||||
"""Return the name of the group."""
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
"""Set Group name."""
|
||||
self._name = value
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the group."""
|
||||
@@ -307,19 +470,16 @@ class Group(Entity):
|
||||
"""Return the icon of the group."""
|
||||
return self._icon
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_visible(self, visible):
|
||||
"""Change visibility of the group."""
|
||||
if self._visible != visible:
|
||||
self._visible = visible
|
||||
yield from self.async_update_ha_state()
|
||||
@icon.setter
|
||||
def icon(self, value):
|
||||
"""Set Icon for group."""
|
||||
self._icon = value
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""If group should be hidden or not."""
|
||||
# Visibility from set_visibility service overrides
|
||||
if self._visible:
|
||||
return not self._user_defined or self._view
|
||||
if self.visible and not self.view:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -331,10 +491,10 @@ class Group(Entity):
|
||||
}
|
||||
if not self._user_defined:
|
||||
data[ATTR_AUTO] = True
|
||||
if self._view:
|
||||
if self.view:
|
||||
data[ATTR_VIEW] = True
|
||||
if self._control:
|
||||
data[ATTR_CONTROL] = self._control
|
||||
if self.control:
|
||||
data[ATTR_CONTROL] = self.control
|
||||
return data
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for Homematic devices.
|
||||
Support for HomeMatic devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/homematic/
|
||||
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.27']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.28']
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
|
||||
@@ -228,7 +228,7 @@ def set_var_value(hass, entity_id, value):
|
||||
|
||||
|
||||
def set_dev_value(hass, address, channel, param, value, proxy=None):
|
||||
"""Send virtual keypress to the Homematic controlller."""
|
||||
"""Call setValue XML-RPC method of supplied proxy."""
|
||||
data = {
|
||||
ATTR_ADDRESS: address,
|
||||
ATTR_CHANNEL: channel,
|
||||
@@ -245,16 +245,15 @@ def reconnect(hass):
|
||||
hass.services.call(DOMAIN, SERVICE_RECONNECT, {})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Set up the Homematic component."""
|
||||
from pyhomematic import HMConnection
|
||||
|
||||
hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY)
|
||||
hass.data[DATA_DEVINIT] = {}
|
||||
hass.data[DATA_STORE] = []
|
||||
hass.data[DATA_STORE] = set()
|
||||
|
||||
# Create hosts list for pyhomematic
|
||||
# Create hosts-dictionary for pyhomematic
|
||||
remotes = {}
|
||||
hosts = {}
|
||||
for rname, rconfig in config[DOMAIN][CONF_HOSTS].items():
|
||||
@@ -286,10 +285,10 @@ def setup(hass, config):
|
||||
interface_id='homeassistant'
|
||||
)
|
||||
|
||||
# Start server thread, connect to peer, initialize to receive events
|
||||
# Start server thread, connect to hosts, initialize to receive events
|
||||
hass.data[DATA_HOMEMATIC].start()
|
||||
|
||||
# Stops server when Homeassistant is shutting down
|
||||
# Stops server when HASS is shutting down
|
||||
hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop)
|
||||
|
||||
@@ -299,12 +298,12 @@ def setup(hass, config):
|
||||
entity_hubs.append(HMHub(
|
||||
hass, hub_data[CONF_NAME], hub_data[CONF_VARIABLES]))
|
||||
|
||||
# Register Homematic services
|
||||
# Register HomeMatic services
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def _hm_service_virtualkey(service):
|
||||
"""Service handle virtualkey services."""
|
||||
"""Service to handle virtualkey servicecalls."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
channel = service.data.get(ATTR_CHANNEL)
|
||||
param = service.data.get(ATTR_PARAM)
|
||||
@@ -315,18 +314,18 @@ def setup(hass, config):
|
||||
_LOGGER.error("%s not found for service virtualkey!", address)
|
||||
return
|
||||
|
||||
# If param exists for this device
|
||||
# Parameter doesn't exist for device
|
||||
if param not in hmdevice.ACTIONNODE:
|
||||
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
||||
return
|
||||
|
||||
# Channel exists?
|
||||
# Channel doesn't exist for device
|
||||
if channel not in hmdevice.ACTIONNODE[param]:
|
||||
_LOGGER.error("%i is not a channel in hm device %s",
|
||||
channel, address)
|
||||
return
|
||||
|
||||
# Call key
|
||||
# Call parameter
|
||||
hmdevice.actionNodeData(param, True, channel)
|
||||
|
||||
hass.services.register(
|
||||
@@ -335,7 +334,7 @@ def setup(hass, config):
|
||||
schema=SCHEMA_SERVICE_VIRTUALKEY)
|
||||
|
||||
def _service_handle_value(service):
|
||||
"""Set value on homematic variable."""
|
||||
"""Service to call setValue method for HomeMatic system variable."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
name = service.data[ATTR_NAME]
|
||||
value = service.data[ATTR_VALUE]
|
||||
@@ -347,7 +346,7 @@ def setup(hass, config):
|
||||
entities = entity_hubs
|
||||
|
||||
if not entities:
|
||||
_LOGGER.error("Homematic controller not found!")
|
||||
_LOGGER.error("No HomeMatic hubs available")
|
||||
return
|
||||
|
||||
for hub in entities:
|
||||
@@ -359,7 +358,7 @@ def setup(hass, config):
|
||||
schema=SCHEMA_SERVICE_SET_VAR_VALUE)
|
||||
|
||||
def _service_handle_reconnect(service):
|
||||
"""Reconnect to all homematic hubs."""
|
||||
"""Service to reconnect all HomeMatic hubs."""
|
||||
hass.data[DATA_HOMEMATIC].reconnect()
|
||||
|
||||
hass.services.register(
|
||||
@@ -368,7 +367,7 @@ def setup(hass, config):
|
||||
schema=SCHEMA_SERVICE_RECONNECT)
|
||||
|
||||
def _service_handle_device(service):
|
||||
"""Service handle set_dev_value services."""
|
||||
"""Service to call setValue method for HomeMatic devices."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
channel = service.data.get(ATTR_CHANNEL)
|
||||
param = service.data.get(ATTR_PARAM)
|
||||
@@ -380,7 +379,6 @@ def setup(hass, config):
|
||||
_LOGGER.error("%s not found!", address)
|
||||
return
|
||||
|
||||
# Call key
|
||||
hmdevice.setValue(param, value, channel)
|
||||
|
||||
hass.services.register(
|
||||
@@ -392,10 +390,9 @@ def setup(hass, config):
|
||||
|
||||
|
||||
def _system_callback_handler(hass, config, src, *args):
|
||||
"""Handle the callback."""
|
||||
"""System callback handler."""
|
||||
# New devices available at hub
|
||||
if src == 'newDevices':
|
||||
_LOGGER.debug("newDevices with: %s", args)
|
||||
# pylint: disable=unused-variable
|
||||
(interface_id, dev_descriptions) = args
|
||||
proxy = interface_id.split('-')[-1]
|
||||
|
||||
@@ -403,34 +400,25 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
if not hass.data[DATA_DEVINIT][proxy]:
|
||||
return
|
||||
|
||||
# Get list of all keys of the devices (ignoring channels)
|
||||
key_dict = {}
|
||||
addresses = []
|
||||
for dev in dev_descriptions:
|
||||
key_dict[dev['ADDRESS'].split(':')[0]] = True
|
||||
|
||||
# Remove device they allready init by HA
|
||||
tmp_devs = key_dict.copy()
|
||||
for dev in tmp_devs:
|
||||
if dev in hass.data[DATA_STORE]:
|
||||
del key_dict[dev]
|
||||
else:
|
||||
hass.data[DATA_STORE].append(dev)
|
||||
address = dev['ADDRESS'].split(':')[0]
|
||||
if address not in hass.data[DATA_STORE]:
|
||||
hass.data[DATA_STORE].add(address)
|
||||
addresses.append(address)
|
||||
|
||||
# Register EVENTS
|
||||
# Search all device with a EVENTNODE that include data
|
||||
# Search all devices with an EVENTNODE that includes data
|
||||
bound_event_callback = partial(_hm_event_handler, hass, proxy)
|
||||
for dev in key_dict:
|
||||
for dev in addresses:
|
||||
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev)
|
||||
|
||||
# Have events?
|
||||
if hmdevice.EVENTNODE:
|
||||
_LOGGER.debug("Register Events from %s", dev)
|
||||
hmdevice.setEventCallback(
|
||||
callback=bound_event_callback, bequeath=True)
|
||||
|
||||
# If configuration allows autodetection of devices,
|
||||
# all devices not configured are added.
|
||||
if key_dict:
|
||||
# Create HASS entities
|
||||
if addresses:
|
||||
for component_name, discovery_type in (
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
('light', DISCOVER_LIGHTS),
|
||||
@@ -440,18 +428,18 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
('climate', DISCOVER_CLIMATE)):
|
||||
# Get all devices of a specific type
|
||||
found_devices = _get_devices(
|
||||
hass, discovery_type, key_dict, proxy)
|
||||
hass, discovery_type, addresses, proxy)
|
||||
|
||||
# When devices of this type are found
|
||||
# they are setup in HA and an event is fired
|
||||
# they are setup in HASS and an discovery event is fired
|
||||
if found_devices:
|
||||
# Fire discovery event
|
||||
discovery.load_platform(hass, component_name, DOMAIN, {
|
||||
ATTR_DISCOVER_DEVICES: found_devices
|
||||
}, config)
|
||||
|
||||
# Homegear error message
|
||||
elif src == 'error':
|
||||
_LOGGER.debug("Error: %s", args)
|
||||
_LOGGER.error("Error: %s", args)
|
||||
(interface_id, errorcode, message) = args
|
||||
hass.bus.fire(EVENT_ERROR, {
|
||||
ATTR_ERRORCODE: errorcode,
|
||||
@@ -460,7 +448,7 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
|
||||
|
||||
def _get_devices(hass, discovery_type, keys, proxy):
|
||||
"""Get the Homematic devices for given discovery_type."""
|
||||
"""Get the HomeMatic devices for given discovery_type."""
|
||||
device_arr = []
|
||||
|
||||
for key in keys:
|
||||
@@ -468,11 +456,11 @@ def _get_devices(hass, discovery_type, keys, proxy):
|
||||
class_name = device.__class__.__name__
|
||||
metadata = {}
|
||||
|
||||
# Class supported by discovery type
|
||||
# Class not supported by discovery type
|
||||
if class_name not in HM_DEVICE_TYPES[discovery_type]:
|
||||
continue
|
||||
|
||||
# Load metadata if needed to generate a param list
|
||||
# Load metadata needed to generate a parameter list
|
||||
if discovery_type == DISCOVER_SENSORS:
|
||||
metadata.update(device.SENSORNODE)
|
||||
elif discovery_type == DISCOVER_BINARY_SENSORS:
|
||||
@@ -480,45 +468,41 @@ def _get_devices(hass, discovery_type, keys, proxy):
|
||||
else:
|
||||
metadata.update({None: device.ELEMENT})
|
||||
|
||||
if metadata:
|
||||
# Generate options for 1...n elements with 1...n params
|
||||
for param, channels in metadata.items():
|
||||
if param in HM_IGNORE_DISCOVERY_NODE:
|
||||
continue
|
||||
# Generate options for 1...n elements with 1...n parameters
|
||||
for param, channels in metadata.items():
|
||||
if param in HM_IGNORE_DISCOVERY_NODE:
|
||||
continue
|
||||
|
||||
# Add devices
|
||||
_LOGGER.debug("%s: Handling %s: %s: %s",
|
||||
discovery_type, key, param, channels)
|
||||
for channel in channels:
|
||||
name = _create_ha_name(
|
||||
name=device.NAME, channel=channel, param=param,
|
||||
count=len(channels)
|
||||
)
|
||||
device_dict = {
|
||||
CONF_PLATFORM: "homematic",
|
||||
ATTR_ADDRESS: key,
|
||||
ATTR_PROXY: proxy,
|
||||
ATTR_NAME: name,
|
||||
ATTR_CHANNEL: channel
|
||||
}
|
||||
if param is not None:
|
||||
device_dict[ATTR_PARAM] = param
|
||||
# Add devices
|
||||
_LOGGER.debug("%s: Handling %s: %s: %s",
|
||||
discovery_type, key, param, channels)
|
||||
for channel in channels:
|
||||
name = _create_ha_name(
|
||||
name=device.NAME, channel=channel, param=param,
|
||||
count=len(channels)
|
||||
)
|
||||
device_dict = {
|
||||
CONF_PLATFORM: "homematic",
|
||||
ATTR_ADDRESS: key,
|
||||
ATTR_PROXY: proxy,
|
||||
ATTR_NAME: name,
|
||||
ATTR_CHANNEL: channel
|
||||
}
|
||||
if param is not None:
|
||||
device_dict[ATTR_PARAM] = param
|
||||
|
||||
# Add new device
|
||||
try:
|
||||
DEVICE_SCHEMA(device_dict)
|
||||
device_arr.append(device_dict)
|
||||
except vol.MultipleInvalid as err:
|
||||
_LOGGER.error("Invalid device config: %s",
|
||||
str(err))
|
||||
else:
|
||||
_LOGGER.debug("Got no params for %s", key)
|
||||
_LOGGER.debug("%s autodiscovery done: %s", discovery_type, str(device_arr))
|
||||
# Add new device
|
||||
try:
|
||||
DEVICE_SCHEMA(device_dict)
|
||||
device_arr.append(device_dict)
|
||||
except vol.MultipleInvalid as err:
|
||||
_LOGGER.error("Invalid device config: %s",
|
||||
str(err))
|
||||
return device_arr
|
||||
|
||||
|
||||
def _create_ha_name(name, channel, param, count):
|
||||
"""Generate a unique object name."""
|
||||
"""Generate a unique entity id."""
|
||||
# HMDevice is a simple device
|
||||
if count == 1 and param is None:
|
||||
return name
|
||||
@@ -527,11 +511,11 @@ def _create_ha_name(name, channel, param, count):
|
||||
if count > 1 and param is None:
|
||||
return "{} {}".format(name, channel)
|
||||
|
||||
# With multiple param first elements
|
||||
# With multiple parameters on first channel
|
||||
if count == 1 and param is not None:
|
||||
return "{} {}".format(name, param)
|
||||
|
||||
# Multiple param on object with multiple elements
|
||||
# Multiple parameters with multiple channels
|
||||
if count > 1 and param is not None:
|
||||
return "{} {} {}".format(name, channel, param)
|
||||
|
||||
@@ -546,14 +530,14 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
|
||||
_LOGGER.error("Event handling channel convert error!")
|
||||
return
|
||||
|
||||
# is not a event?
|
||||
# Return if not an event supported by device
|
||||
if attribute not in hmdevice.EVENTNODE:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Event %s for %s channel %i", attribute,
|
||||
hmdevice.NAME, channel)
|
||||
|
||||
# keypress event
|
||||
# Keypress event
|
||||
if attribute in HM_PRESS_EVENTS:
|
||||
hass.bus.fire(EVENT_KEYPRESS, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
@@ -562,7 +546,7 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
|
||||
})
|
||||
return
|
||||
|
||||
# impulse event
|
||||
# Impulse event
|
||||
if attribute in HM_IMPULSE_EVENTS:
|
||||
hass.bus.fire(EVENT_IMPULSE, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
@@ -574,7 +558,7 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
|
||||
|
||||
|
||||
def _device_from_servicecall(hass, service):
|
||||
"""Extract homematic device from service call."""
|
||||
"""Extract HomeMatic device from service call."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
proxy = service.data.get(ATTR_PROXY)
|
||||
if address == 'BIDCOS-RF':
|
||||
@@ -589,10 +573,10 @@ def _device_from_servicecall(hass, service):
|
||||
|
||||
|
||||
class HMHub(Entity):
|
||||
"""The Homematic hub. I.e. CCU2/HomeGear."""
|
||||
"""The HomeMatic hub. (CCU2/HomeGear)."""
|
||||
|
||||
def __init__(self, hass, name, use_variables):
|
||||
"""Initialize Homematic hub."""
|
||||
"""Initialize HomeMatic hub."""
|
||||
self.hass = hass
|
||||
self.entity_id = "{}.{}".format(DOMAIN, name.lower())
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
@@ -601,7 +585,7 @@ class HMHub(Entity):
|
||||
self._state = STATE_UNKNOWN
|
||||
self._use_variables = use_variables
|
||||
|
||||
# load data
|
||||
# Load data
|
||||
track_time_interval(hass, self._update_hub, SCAN_INTERVAL_HUB)
|
||||
self._update_hub(None)
|
||||
|
||||
@@ -617,7 +601,7 @@ class HMHub(Entity):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. Homematic Hub object update variable."""
|
||||
"""Return false. HomeMatic Hub object updates variables."""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -660,7 +644,7 @@ class HMHub(Entity):
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def hm_set_variable(self, name, value):
|
||||
"""Set variable on homematic controller."""
|
||||
"""Set variable value on CCU/Homegear."""
|
||||
if name not in self._variables:
|
||||
_LOGGER.error("Variable %s not found on %s", name, self.name)
|
||||
return
|
||||
@@ -676,10 +660,10 @@ class HMHub(Entity):
|
||||
|
||||
|
||||
class HMDevice(Entity):
|
||||
"""The Homematic device base object."""
|
||||
"""The HomeMatic device base object."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a generic Homematic device."""
|
||||
"""Initialize a generic HomeMatic device."""
|
||||
self.hass = hass
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._name = config.get(ATTR_NAME)
|
||||
@@ -692,13 +676,13 @@ class HMDevice(Entity):
|
||||
self._connected = False
|
||||
self._available = False
|
||||
|
||||
# Set param to uppercase
|
||||
# Set parameter to uppercase
|
||||
if self._state:
|
||||
self._state = self._state.upper()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. Homematic states are pushed by the XML RPC Server."""
|
||||
"""Return false. HomeMatic states are pushed by the XML-RPC Server."""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -721,49 +705,44 @@ class HMDevice(Entity):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {}
|
||||
|
||||
# no data available to create
|
||||
# No data available
|
||||
if not self.available:
|
||||
return attr
|
||||
|
||||
# Generate an attributes list
|
||||
# Generate a dictionary with attributes
|
||||
for node, data in HM_ATTRIBUTE_SUPPORT.items():
|
||||
# Is an attributes and exists for this object
|
||||
# Is an attribute and exists for this object
|
||||
if node in self._data:
|
||||
value = data[1].get(self._data[node], self._data[node])
|
||||
attr[data[0]] = value
|
||||
|
||||
# static attributes
|
||||
# Static attributes
|
||||
attr['id'] = self._hmdevice.ADDRESS
|
||||
attr['proxy'] = self._proxy
|
||||
|
||||
return attr
|
||||
|
||||
def link_homematic(self):
|
||||
"""Connect to Homematic."""
|
||||
# Device is already linked
|
||||
"""Connect to HomeMatic."""
|
||||
if self._connected:
|
||||
return True
|
||||
|
||||
# Init
|
||||
# Initialize
|
||||
self._hmdevice = self._homematic.devices[self._proxy][self._address]
|
||||
self._connected = True
|
||||
|
||||
# Check if Homematic class is okay for HA class
|
||||
_LOGGER.info("Start linking %s to %s", self._address, self._name)
|
||||
try:
|
||||
# Init datapoints of this object
|
||||
# Initialize datapoints of this object
|
||||
self._init_data()
|
||||
if self.hass.data[DATA_DELAY]:
|
||||
# We delay / pause loading of data to avoid overloading
|
||||
# of CCU / Homegear when doing auto detection
|
||||
# We optionally delay / pause loading of data to avoid
|
||||
# overloading of CCU / Homegear
|
||||
time.sleep(self.hass.data[DATA_DELAY])
|
||||
self._load_data_from_hm()
|
||||
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
|
||||
|
||||
# Link events from pyhomatic
|
||||
# Link events from pyhomematic
|
||||
self._subscribe_homematic_events()
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
_LOGGER.debug("%s linking done", self._name)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
self._connected = False
|
||||
@@ -774,29 +753,28 @@ class HMDevice(Entity):
|
||||
"""Handle all pyhomematic device events."""
|
||||
_LOGGER.debug("%s received event '%s' value: %s", self._name,
|
||||
attribute, value)
|
||||
have_change = False
|
||||
has_changed = False
|
||||
|
||||
# Is data needed for this instance?
|
||||
if attribute in self._data:
|
||||
# Did data change?
|
||||
if self._data[attribute] != value:
|
||||
self._data[attribute] = value
|
||||
have_change = True
|
||||
has_changed = True
|
||||
|
||||
# If available it has changed
|
||||
# Availability has changed
|
||||
if attribute == 'UNREACH':
|
||||
self._available = bool(value)
|
||||
have_change = True
|
||||
has_changed = True
|
||||
|
||||
# If it has changed data point, update HA
|
||||
if have_change:
|
||||
_LOGGER.debug("%s update_ha_state after '%s'", self._name,
|
||||
attribute)
|
||||
# If it has changed data point, update HASS
|
||||
if has_changed:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _subscribe_homematic_events(self):
|
||||
"""Subscribe all required events to handle job."""
|
||||
channels_to_sub = {0: True} # add channel 0 for UNREACH
|
||||
channels_to_sub = set()
|
||||
channels_to_sub.add(0) # Add channel 0 for UNREACH
|
||||
|
||||
# Push data to channels_to_sub from hmdevice metadata
|
||||
for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE,
|
||||
@@ -814,8 +792,7 @@ class HMDevice(Entity):
|
||||
|
||||
# Prepare for subscription
|
||||
try:
|
||||
if int(channel) >= 0:
|
||||
channels_to_sub.update({int(channel): True})
|
||||
channels_to_sub.add(int(channel))
|
||||
except (ValueError, TypeError):
|
||||
_LOGGER.error("Invalid channel in metadata from %s",
|
||||
self._name)
|
||||
@@ -858,14 +835,14 @@ class HMDevice(Entity):
|
||||
return None
|
||||
|
||||
def _init_data(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
# Add all attributes to data dict
|
||||
"""Generate a data dict (self._data) from the HomeMatic metadata."""
|
||||
# Add all attributes to data dictionary
|
||||
for data_note in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data.update({data_note: STATE_UNKNOWN})
|
||||
|
||||
# init device specified data
|
||||
# Initialize device specific data
|
||||
self._init_data_struct()
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict from the Homematic device metadata."""
|
||||
"""Generate a data dictionary from the HomeMatic device metadata."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -51,7 +51,7 @@ CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
||||
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
|
||||
CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
|
||||
|
||||
# TLS configuation follows the best-practice guidelines specified here:
|
||||
# TLS configuration follows the best-practice guidelines specified here:
|
||||
# https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
# Intermediate guidelines are followed.
|
||||
SSL_VERSION = ssl.PROTOCOL_SSLv23
|
||||
@@ -339,7 +339,7 @@ class HomeAssistantWSGI(object):
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop(self):
|
||||
"""Stop the wsgi server."""
|
||||
"""Stop the WSGI server."""
|
||||
if self.server:
|
||||
self.server.close()
|
||||
yield from self.server.wait_closed()
|
||||
|
||||
@@ -19,6 +19,8 @@ from .const import (
|
||||
KEY_FAILED_LOGIN_ATTEMPTS)
|
||||
from .util import get_real_ip
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NOTIFICATION_ID_BAN = 'ip-ban'
|
||||
NOTIFICATION_ID_LOGIN = 'http-login'
|
||||
|
||||
@@ -29,8 +31,6 @@ SCHEMA_IP_BAN_ENTRY = vol.Schema({
|
||||
vol.Optional('banned_at'): vol.Any(None, cv.datetime)
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def ban_middleware(app, handler):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user