forked from home-assistant/core
Compare commits
284 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4a7f1160 | ||
|
|
dc08852fc2 | ||
|
|
3377f30613 | ||
|
|
84ca4d2a21 | ||
|
|
1366c93c83 | ||
|
|
e5e2a151aa | ||
|
|
bd1e533409 | ||
|
|
21e82bd037 | ||
|
|
af9a0e8fea | ||
|
|
abc5c3e128 | ||
|
|
192db5bec3 | ||
|
|
b8eaec565a | ||
|
|
e0f35c0279 | ||
|
|
2eeeb9075a | ||
|
|
71ee290bfd | ||
|
|
7aad93e90d | ||
|
|
a65f22378e | ||
|
|
bb9db28c95 | ||
|
|
d10f017441 | ||
|
|
b6e0286d71 | ||
|
|
4451d2e847 | ||
|
|
229000b834 | ||
|
|
9704057959 | ||
|
|
effb9e9d23 | ||
|
|
effbb3bd4c | ||
|
|
471501d386 | ||
|
|
ef94b5c77a | ||
|
|
60dcc9a5c0 | ||
|
|
5b4862cc3c | ||
|
|
fbf945c18b | ||
|
|
609c25691a | ||
|
|
6e77877743 | ||
|
|
7b105a2150 | ||
|
|
ee57a823af | ||
|
|
04b1621b65 | ||
|
|
f5e24cb0bb | ||
|
|
ac72dea09a | ||
|
|
2f474a0ed8 | ||
|
|
7a4cc8e082 | ||
|
|
92dc76773a | ||
|
|
fe4abc8454 | ||
|
|
821d01f82c | ||
|
|
b453834b2f | ||
|
|
97f14015ea | ||
|
|
4fb25cf16d | ||
|
|
e7b5c5812c | ||
|
|
2ac423bd9d | ||
|
|
ec7ca9a560 | ||
|
|
cb298123d4 | ||
|
|
c5bf4fe339 | ||
|
|
57c5ed33ee | ||
|
|
3be0103259 | ||
|
|
614b5da170 | ||
|
|
acf6d4ab82 | ||
|
|
d3acb25070 | ||
|
|
222ad3ab6d | ||
|
|
5ae2bcdbb7 | ||
|
|
6c9742afc4 | ||
|
|
cf924cd14d | ||
|
|
f2267437df | ||
|
|
233920f22c | ||
|
|
7536e825fa | ||
|
|
e12a9eaadd | ||
|
|
fb184b4b6f | ||
|
|
63ff173305 | ||
|
|
903e6b5aee | ||
|
|
46ce26eb7a | ||
|
|
b1bba3675d | ||
|
|
ed5d10448e | ||
|
|
652c006cbc | ||
|
|
b67c5df525 | ||
|
|
a7d5a8d93e | ||
|
|
c48c2b00a8 | ||
|
|
9bc5cd2d4b | ||
|
|
ecf3a9cb36 | ||
|
|
074e31bcf9 | ||
|
|
63cc658010 | ||
|
|
8682f21fc5 | ||
|
|
aa28e6727d | ||
|
|
12129f0e6a | ||
|
|
8a7cfce67b | ||
|
|
5e71e9b826 | ||
|
|
db8bb53984 | ||
|
|
692f4c293b | ||
|
|
da37380410 | ||
|
|
fa4aa2244e | ||
|
|
c63bdd5afe | ||
|
|
20a9899354 | ||
|
|
fe6a4b8ae5 | ||
|
|
143044f8f1 | ||
|
|
d655c0e358 | ||
|
|
46e030662d | ||
|
|
5779d64e98 | ||
|
|
83a5f932d1 | ||
|
|
a12fa2e5bf | ||
|
|
ee37fc344b | ||
|
|
8cc0748db3 | ||
|
|
0ecceb601b | ||
|
|
2a1a5e53a1 | ||
|
|
c8b782189e | ||
|
|
74016c4179 | ||
|
|
c30c8df449 | ||
|
|
58de661ad5 | ||
|
|
f4a97db783 | ||
|
|
b220ceec9c | ||
|
|
fb796b5481 | ||
|
|
ea5bec3ef4 | ||
|
|
8185587100 | ||
|
|
061a38cc3b | ||
|
|
23fc5e2c9f | ||
|
|
6496c38ce6 | ||
|
|
3363b88a73 | ||
|
|
2e17d0926a | ||
|
|
85ac50cc77 | ||
|
|
da61b18392 | ||
|
|
8a88af20da | ||
|
|
f8527e9773 | ||
|
|
7977996c0d | ||
|
|
22681fbe08 | ||
|
|
1e655eea74 | ||
|
|
8d940fb585 | ||
|
|
afe3dd8dbb | ||
|
|
bf96f28e95 | ||
|
|
5cba3085b4 | ||
|
|
407a419c83 | ||
|
|
4ab778fd97 | ||
|
|
ee7d4710c4 | ||
|
|
3a6434f566 | ||
|
|
a2f5b630d6 | ||
|
|
3f2fa0ed5a | ||
|
|
865865ca0f | ||
|
|
05ced33648 | ||
|
|
b4165fe9f3 | ||
|
|
47aa8c387a | ||
|
|
2b94857ffd | ||
|
|
7461c57542 | ||
|
|
632f9a21b6 | ||
|
|
da44f80b32 | ||
|
|
0bf5021c2c | ||
|
|
8fb49e8687 | ||
|
|
b82003ae08 | ||
|
|
5f8dc8af20 | ||
|
|
c13fdd23c1 | ||
|
|
e6e0e5263a | ||
|
|
0981956caa | ||
|
|
d267fc608f | ||
|
|
e077998d38 | ||
|
|
d3bc8519c0 | ||
|
|
d3adc6ddfb | ||
|
|
a3f586d097 | ||
|
|
f8c7fd212f | ||
|
|
b1f3492fd0 | ||
|
|
7123ec14be | ||
|
|
8e4394f173 | ||
|
|
5e56bc7464 | ||
|
|
ed20f7e359 | ||
|
|
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 | ||
|
|
6a017efc0e | ||
|
|
18935440ed | ||
|
|
2ba6b3a2ab | ||
|
|
2438c6b7c2 | ||
|
|
32a84f1466 | ||
|
|
0002a895ca | ||
|
|
d0b43b187a | ||
|
|
33d381731f | ||
|
|
18f81d7824 | ||
|
|
844c8149d7 | ||
|
|
7617864ba5 | ||
|
|
58c234466c | ||
|
|
9071946e87 | ||
|
|
b24aa24f6a | ||
|
|
1fde234c78 | ||
|
|
afb9cba806 | ||
|
|
1c2f4866e2 | ||
|
|
e90ae2fb75 |
113
.coveragerc
113
.coveragerc
@@ -11,9 +11,15 @@ omit =
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
homeassistant/components/amcrest.py
|
||||
homeassistant/components/*/amcrest.py
|
||||
|
||||
homeassistant/components/apcupsd.py
|
||||
homeassistant/components/*/apcupsd.py
|
||||
|
||||
homeassistant/components/apple_tv.py
|
||||
homeassistant/components/*/apple_tv.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
@@ -35,23 +41,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,12 +83,21 @@ 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/lametric.py
|
||||
homeassistant/components/*/lametric.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
@@ -80,15 +107,27 @@ omit =
|
||||
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
|
||||
|
||||
@@ -116,6 +155,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
|
||||
|
||||
@@ -131,6 +173,9 @@ omit =
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
homeassistant/components/velux.py
|
||||
homeassistant/components/*/velux.py
|
||||
|
||||
homeassistant/components/vera.py
|
||||
homeassistant/components/*/vera.py
|
||||
|
||||
@@ -148,45 +193,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
|
||||
@@ -205,7 +223,6 @@ omit =
|
||||
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
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
@@ -224,11 +241,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
|
||||
@@ -242,6 +259,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
|
||||
@@ -263,12 +281,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
|
||||
@@ -278,7 +294,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
|
||||
@@ -296,8 +312,8 @@ omit =
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/media_extractor.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/apple_tv.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
@@ -312,7 +328,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
|
||||
@@ -335,6 +350,7 @@ omit =
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vizio.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
@@ -342,13 +358,13 @@ 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
|
||||
@@ -371,20 +387,22 @@ omit =
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/prometheus.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/amcrest.py
|
||||
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/citybikes.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
@@ -398,6 +416,7 @@ omit =
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
@@ -419,6 +438,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
|
||||
@@ -443,6 +463,7 @@ omit =
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/opensky.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/otp.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
@@ -473,6 +494,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
|
||||
@@ -480,6 +502,7 @@ 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
|
||||
@@ -489,7 +512,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
|
||||
@@ -503,6 +525,7 @@ omit =
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/switch/xiaomi_vacuum.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
|
||||
@@ -1,2 +1,14 @@
|
||||
.tox
|
||||
# General files
|
||||
.git
|
||||
.github
|
||||
config
|
||||
|
||||
# Test related files
|
||||
.tox
|
||||
|
||||
# Other virtualization methods
|
||||
venv
|
||||
.vagrant
|
||||
|
||||
# Temporary files
|
||||
**/__pycache__
|
||||
@@ -16,8 +16,8 @@ matrix:
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.6-dev"
|
||||
env: TOXENV=py36
|
||||
# - python: "3.6-dev"
|
||||
# env: TOXENV=py36
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
# allow_failures:
|
||||
|
||||
41
CODEOWNERS
Normal file
41
CODEOWNERS
Normal file
@@ -0,0 +1,41 @@
|
||||
# People marked here will be automatically requested for a review
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
setup.py @home-assistant/core
|
||||
homeassistant/*.py @home-assistant/core
|
||||
homeassistant/helpers/* @home-assistant/core
|
||||
homeassistant/util/* @home-assistant/core
|
||||
homeassistant/components/api.py @home-assistant/core
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/configurator.py @home-assistant/core
|
||||
homeassistant/components/group.py @home-assistant/core
|
||||
homeassistant/components/history.py @home-assistant/core
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/input_*.py @home-assistant/core
|
||||
homeassistant/components/introduction.py @home-assistant/core
|
||||
homeassistant/components/logger.py @home-assistant/core
|
||||
homeassistant/components/mqtt/* @home-assistant/core
|
||||
homeassistant/components/panel_custom.py @home-assistant/core
|
||||
homeassistant/components/panel_iframe.py @home-assistant/core
|
||||
homeassistant/components/persistent_notification.py @home-assistant/core
|
||||
homeassistant/components/scene/__init__.py @home-assistant/core
|
||||
homeassistant/components/scene/hass.py @home-assistant/core
|
||||
homeassistant/components/script.py @home-assistant/core
|
||||
homeassistant/components/shell_command.py @home-assistant/core
|
||||
homeassistant/components/sun.py @home-assistant/core
|
||||
homeassistant/components/updater.py @home-assistant/core
|
||||
homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/websocket_api.py @home-assistant/core
|
||||
homeassistant/components/zone.py @home-assistant/core
|
||||
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
|
||||
# Indiviudal components
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
10
Dockerfile
10
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>
|
||||
|
||||
@@ -21,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 . .
|
||||
|
||||
@@ -229,8 +229,8 @@ def cmdline() -> List[str]:
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
arg != '--daemon']
|
||||
else:
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
|
||||
@@ -39,19 +39,19 @@ def is_on(hass, entity_id=None):
|
||||
else:
|
||||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = ha.split_entity_id(entity_id)[0]
|
||||
for ent_id in entity_ids:
|
||||
domain = ha.split_entity_id(ent_id)[0]
|
||||
|
||||
module = get_component(domain)
|
||||
|
||||
try:
|
||||
if module.is_on(hass, entity_id):
|
||||
if module.is_on(hass, ent_id):
|
||||
return True
|
||||
|
||||
except AttributeError:
|
||||
# module is None or method is_on does not exist
|
||||
_LOGGER.exception("Failed to call %s.is_on for %s",
|
||||
module, entity_id)
|
||||
module, ent_id)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -92,8 +92,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
|
||||
@@ -113,8 +113,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
else:
|
||||
return '^\\d{4,6}$'
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -99,8 +99,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return self._pre_trigger_state
|
||||
return self._pre_trigger_state
|
||||
|
||||
return self._state
|
||||
|
||||
|
||||
@@ -80,8 +80,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN,
|
||||
CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.7']
|
||||
REQUIREMENTS = ['total_connect_client==0.11']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -39,10 +39,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
"""Representation a Wink camera alarm."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink alarm."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
|
||||
@@ -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,20 +255,30 @@ 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 async_turn_on(self):
|
||||
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, **kwargs):
|
||||
"""Async Unacknowledge alert."""
|
||||
_LOGGER.debug("Reset Alert: %s", self._name)
|
||||
self._ack = False
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self):
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Async Acknowledge alert."""
|
||||
_LOGGER.debug("Acknowledged Alert: %s", self._name)
|
||||
self._ack = True
|
||||
|
||||
149
homeassistant/components/amcrest.py
Normal file
149
homeassistant/components/amcrest.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
This component provides basic support for Amcrest IP cameras.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/amcrest/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.0']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
DOMAIN = 'amcrest'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
AUTHENTICATION_LIST = {
|
||||
'basic': 'basic'
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSORS = {
|
||||
'motion_detector': ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera
|
||||
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
for device in amcrest_cams:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
try:
|
||||
camera.current_time
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||
name = device.get(CONF_NAME)
|
||||
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
|
||||
|
||||
username = device.get(CONF_USERNAME)
|
||||
password = device.get(CONF_PASSWORD)
|
||||
|
||||
# currently aiohttp only works with basic authentication
|
||||
# only valid for mjpeg streaming
|
||||
if username is not None and password is not None:
|
||||
if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION:
|
||||
authentication = aiohttp.BasicAuth(username, password)
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_AUTHENTICATION: authentication,
|
||||
CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments,
|
||||
CONF_NAME: name,
|
||||
CONF_RESOLUTION: resolution,
|
||||
CONF_STREAM_SOURCE: stream_source,
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
return True
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['apcaccess==0.0.10']
|
||||
REQUIREMENTS = ['apcaccess==0.0.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -198,8 +198,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
@@ -213,7 +212,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
|
||||
new_state = data.get('state')
|
||||
|
||||
if not new_state:
|
||||
if new_state is None:
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = data.get('attributes')
|
||||
@@ -237,8 +236,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
"""Remove entity."""
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
|
||||
259
homeassistant/components/apple_tv.py
Normal file
259
homeassistant/components/apple_tv.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
Support for Apple TV.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/apple_tv/
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.components.discovery import SERVICE_APPLE_TV
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'apple_tv'
|
||||
|
||||
SERVICE_SCAN = 'apple_tv_scan'
|
||||
SERVICE_AUTHENTICATE = 'apple_tv_authenticate'
|
||||
|
||||
ATTR_ATV = 'atv'
|
||||
ATTR_POWER = 'power'
|
||||
|
||||
CONF_LOGIN_ID = 'login_id'
|
||||
CONF_START_OFF = 'start_off'
|
||||
CONF_CREDENTIALS = 'credentials'
|
||||
|
||||
DEFAULT_NAME = 'Apple TV'
|
||||
|
||||
DATA_APPLE_TV = 'data_apple_tv'
|
||||
DATA_ENTITIES = 'data_apple_tv_entities'
|
||||
|
||||
KEY_CONFIG = 'apple_tv_configuring'
|
||||
|
||||
NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification'
|
||||
NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_CREDENTIALS, default=None): cv.string,
|
||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
# Currently no attributes but it might change later
|
||||
APPLE_TV_SCAN_SCHEMA = vol.Schema({})
|
||||
|
||||
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
def request_configuration(hass, config, atv, credentials):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = get_component('configurator')
|
||||
|
||||
@asyncio.coroutine
|
||||
def configuration_callback(callback_data):
|
||||
"""Handle the submitted configuration."""
|
||||
from pyatv import exceptions
|
||||
pin = callback_data.get('pin')
|
||||
notification = get_component('persistent_notification')
|
||||
|
||||
try:
|
||||
yield from atv.airplay.finish_authentication(pin)
|
||||
notification.async_create(
|
||||
hass,
|
||||
'Authentication succeeded!<br /><br />Add the following '
|
||||
'to credentials: in your apple_tv configuration:<br /><br />'
|
||||
'{0}'.format(credentials),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
except exceptions.DeviceAuthenticationError as ex:
|
||||
notification.async_create(
|
||||
hass,
|
||||
'Authentication failed! Did you enter correct PIN?<br /><br />'
|
||||
'Details: {0}'.format(ex),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
|
||||
hass.async_add_job(configurator.request_done, instance)
|
||||
|
||||
instance = configurator.request_config(
|
||||
hass, 'Apple TV Authentication', configuration_callback,
|
||||
description='Please enter PIN code shown on screen.',
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def scan_for_apple_tvs(hass):
|
||||
"""Scan for devices and present a notification of the ones found."""
|
||||
import pyatv
|
||||
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
|
||||
|
||||
devices = []
|
||||
for atv in atvs:
|
||||
login_id = atv.login_id
|
||||
if login_id is None:
|
||||
login_id = 'Home Sharing disabled'
|
||||
devices.append('Name: {0}<br />Host: {1}<br />Login ID: {2}'.format(
|
||||
atv.name, atv.address, login_id))
|
||||
|
||||
if not devices:
|
||||
devices = ['No device(s) found']
|
||||
|
||||
notification = get_component('persistent_notification')
|
||||
notification.async_create(
|
||||
hass,
|
||||
'The following devices were found:<br /><br />' +
|
||||
'<br /><br />'.join(devices),
|
||||
title=NOTIFICATION_SCAN_TITLE,
|
||||
notification_id=NOTIFICATION_SCAN_ID)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the Apple TV component."""
|
||||
if DATA_APPLE_TV not in hass.data:
|
||||
hass.data[DATA_APPLE_TV] = {}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
"""Handler for service calls."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if entity_ids:
|
||||
devices = [device for device in hass.data[DATA_ENTITIES]
|
||||
if device.entity_id in entity_ids]
|
||||
else:
|
||||
devices = hass.data[DATA_ENTITIES]
|
||||
|
||||
for device in devices:
|
||||
atv = device.atv
|
||||
if service.service == SERVICE_AUTHENTICATE:
|
||||
credentials = yield from atv.airplay.generate_credentials()
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
_LOGGER.debug('Generated new credentials: %s', credentials)
|
||||
yield from atv.airplay.start_authentication()
|
||||
hass.async_add_job(request_configuration,
|
||||
hass, config, atv, credentials)
|
||||
elif service.service == SERVICE_SCAN:
|
||||
hass.async_add_job(scan_for_apple_tvs, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def atv_discovered(service, info):
|
||||
"""Setup an Apple TV that was auto discovered."""
|
||||
yield from _setup_atv(hass, {
|
||||
CONF_NAME: info['name'],
|
||||
CONF_HOST: info['host'],
|
||||
CONF_LOGIN_ID: info['properties']['hG'],
|
||||
CONF_START_OFF: False
|
||||
})
|
||||
|
||||
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
||||
|
||||
tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])]
|
||||
if tasks:
|
||||
yield from asyncio.wait(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_SCAN, async_service_handler,
|
||||
descriptions.get(SERVICE_SCAN),
|
||||
schema=APPLE_TV_SCAN_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
|
||||
descriptions.get(SERVICE_AUTHENTICATE),
|
||||
schema=APPLE_TV_AUTHENTICATE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _setup_atv(hass, atv_config):
|
||||
"""Setup an Apple TV."""
|
||||
import pyatv
|
||||
name = atv_config.get(CONF_NAME)
|
||||
host = atv_config.get(CONF_HOST)
|
||||
login_id = atv_config.get(CONF_LOGIN_ID)
|
||||
start_off = atv_config.get(CONF_START_OFF)
|
||||
credentials = atv_config.get(CONF_CREDENTIALS)
|
||||
|
||||
if host in hass.data[DATA_APPLE_TV]:
|
||||
return
|
||||
|
||||
details = pyatv.AppleTVDevice(name, host, login_id)
|
||||
session = async_get_clientsession(hass)
|
||||
atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
|
||||
if credentials:
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
|
||||
power = AppleTVPowerManager(hass, atv, start_off)
|
||||
hass.data[DATA_APPLE_TV][host] = {
|
||||
ATTR_ATV: atv,
|
||||
ATTR_POWER: power
|
||||
}
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'media_player', DOMAIN, atv_config))
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'remote', DOMAIN, atv_config))
|
||||
|
||||
|
||||
class AppleTVPowerManager:
|
||||
"""Manager for global power management of an Apple TV.
|
||||
|
||||
An instance is used per device to share the same power state between
|
||||
several platforms.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, atv, is_off):
|
||||
"""Initialize power manager."""
|
||||
self.hass = hass
|
||||
self.atv = atv
|
||||
self.listeners = []
|
||||
self._is_on = not is_off
|
||||
|
||||
def init(self):
|
||||
"""Initialize power management."""
|
||||
if self._is_on:
|
||||
self.atv.push_updater.start()
|
||||
|
||||
@property
|
||||
def turned_on(self):
|
||||
"""If device is on or off."""
|
||||
return self._is_on
|
||||
|
||||
def set_power_on(self, value):
|
||||
"""Change if a device is on or off."""
|
||||
if value != self._is_on:
|
||||
self._is_on = value
|
||||
if not self._is_on:
|
||||
self.atv.push_updater.stop()
|
||||
else:
|
||||
self.atv.push_updater.start()
|
||||
|
||||
for listener in self.listeners:
|
||||
self.hass.async_add_job(listener.async_update_ha_state())
|
||||
@@ -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(
|
||||
|
||||
@@ -42,8 +42,6 @@ def async_trigger(hass, config, action):
|
||||
},
|
||||
})
|
||||
|
||||
# Do something to call action
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
return async_track_sunrise(hass, call_action, offset)
|
||||
else:
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
|
||||
@@ -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,12 +64,11 @@ 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(
|
||||
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
|
||||
if request.status_code is not 200:
|
||||
if request.status_code != 200:
|
||||
_LOGGER.error("Can't set mode of %s", self._resource)
|
||||
|
||||
@property
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -199,11 +199,10 @@ class FlicButton(BinarySensorDevice):
|
||||
"Queued %s dropped for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return True
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
|
||||
def _on_up_down(self, channel, click_type, was_queued, time_diff):
|
||||
"""Update device state, if event was not queued."""
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.2']
|
||||
REQUIREMENTS = ['pyhik==0.1.3']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
|
||||
@@ -34,8 +34,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.modbus as modbus
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_NAME, CONF_SLAVE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
@@ -18,7 +18,6 @@ DEPENDENCIES = ['modbus']
|
||||
|
||||
CONF_COIL = 'coil'
|
||||
CONF_COILS = 'coils'
|
||||
CONF_SLAVE = 'slave'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COILS): [{
|
||||
@@ -50,6 +49,11 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
self._coil = int(coil)
|
||||
self._value = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
@@ -58,4 +62,10 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
def update(self):
|
||||
"""Update the state of the sensor."""
|
||||
result = modbus.HUB.read_coils(self._slave, self._coil, 1)
|
||||
self._value = result.bits[0]
|
||||
try:
|
||||
self._value = result.bits[0]
|
||||
except AttributeError:
|
||||
_LOGGER.error(
|
||||
'No response from modbus slave %s coil %s',
|
||||
self._slave,
|
||||
self._coil)
|
||||
|
||||
@@ -156,8 +156,7 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
|
||||
elif self._cameratype == 'NOC':
|
||||
return PRESENCE_SENSOR_TYPES.get(self._sensor_name)
|
||||
else:
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -12,13 +12,12 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['octoprint']
|
||||
|
||||
DOMAIN = "octoprint"
|
||||
DEFAULT_NAME = 'OctoPrint'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
@@ -37,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the available OctoPrint binary sensors."""
|
||||
octoprint = get_component('octoprint')
|
||||
octoprint_api = hass.data[DOMAIN]["api"]
|
||||
name = config.get(CONF_NAME)
|
||||
monitored_conditions = config.get(
|
||||
CONF_MONITORED_CONDITIONS, SENSOR_TYPES.keys())
|
||||
@@ -45,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices = []
|
||||
for octo_type in monitored_conditions:
|
||||
new_sensor = OctoPrintBinarySensor(
|
||||
octoprint.OCTOPRINT, octo_type, SENSOR_TYPES[octo_type][2],
|
||||
octoprint_api, octo_type, SENSOR_TYPES[octo_type][2],
|
||||
name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0],
|
||||
SENSOR_TYPES[octo_type][1], 'flags')
|
||||
devices.append(new_sensor)
|
||||
@@ -98,6 +97,3 @@ class OctoPrintBinarySensor(BinarySensorDevice):
|
||||
except requests.exceptions.ConnectionError:
|
||||
# Error calling the api, already logged in api.update()
|
||||
return
|
||||
|
||||
if self._state is None:
|
||||
_LOGGER.warning("Unable to locate value for %s", self.sensor_type)
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.util import dt as dt_util
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_VARIABLE = 'variable'
|
||||
CONF_RESET_DELAY_SEC = 'reset_delay_sec'
|
||||
|
||||
DEFAULT_NAME = 'Pilight Binary Sensor'
|
||||
DEPENDENCIES = ['pilight']
|
||||
@@ -38,7 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default='on'): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default='off'): cv.string,
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean,
|
||||
vol.Optional(CONF_RESET_DELAY_SEC, default=30): cv.positive_int
|
||||
})
|
||||
|
||||
|
||||
@@ -54,6 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
payload=config.get(CONF_PAYLOAD),
|
||||
on_value=config.get(CONF_PAYLOAD_ON),
|
||||
off_value=config.get(CONF_PAYLOAD_OFF),
|
||||
rst_dly_sec=config.get(CONF_RESET_DELAY_SEC),
|
||||
)])
|
||||
else:
|
||||
add_devices([PilightBinarySensor(
|
||||
|
||||
@@ -126,14 +126,14 @@ class PingData(object):
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': ''}
|
||||
else:
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
except (subprocess.CalledProcessError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
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()
|
||||
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,8 +30,7 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
|
||||
return bool(val)
|
||||
elif self._attribute in ['doors', 'windows']:
|
||||
return any([val[key] for key in val if 'Open' in key])
|
||||
else:
|
||||
return val != 'Normal'
|
||||
return val != 'Normal'
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -121,10 +121,6 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Smoke detector."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -136,10 +132,6 @@ class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
class WinkHub(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Hub."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -152,10 +144,6 @@ class WinkHub(WinkBinarySensorDevice):
|
||||
class WinkRemote(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Lutron Connected bulb remote."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -175,10 +163,6 @@ class WinkRemote(WinkBinarySensorDevice):
|
||||
class WinkButton(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay button."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -191,10 +175,6 @@ class WinkButton(WinkBinarySensorDevice):
|
||||
class WinkGang(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay gang."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the gang is connected."""
|
||||
|
||||
@@ -34,10 +34,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||
|
||||
clusters = discovery_info['clusters']
|
||||
in_clusters = discovery_info['in_clusters']
|
||||
|
||||
device_class = None
|
||||
cluster = [c for c in clusters if isinstance(c, IasZone)][0]
|
||||
cluster = in_clusters[IasZone.cluster_id]
|
||||
if discovery_info['new_join']:
|
||||
yield from cluster.bind()
|
||||
ieee = cluster.endpoint.device.application.ieee
|
||||
@@ -64,7 +64,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
super().__init__(**kwargs)
|
||||
self._device_class = device_class
|
||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||
self._ias_zone_cluster = self._clusters[IasZone.cluster_id]
|
||||
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -148,8 +148,7 @@ class CalendarEventDevice(Entity):
|
||||
if 'date' in date:
|
||||
return dt.start_of_local_day(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time.min))
|
||||
else:
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -196,8 +266,23 @@ class Camera(Entity):
|
||||
return STATE_RECORDING
|
||||
elif self.is_streaming:
|
||||
return STATE_STREAMING
|
||||
else:
|
||||
return STATE_IDLE
|
||||
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):
|
||||
@@ -212,6 +297,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 +357,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)
|
||||
|
||||
|
||||
@@ -7,108 +7,59 @@ https://home-assistant.io/components/camera.amcrest/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.amcrest import (
|
||||
STREAM_SOURCE_LIST, TIMEOUT)
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web,
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.0']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
DEPENDENCIES = ['amcrest', 'ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'mjpeg'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
TIMEOUT = 5
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up an Amcrest IP Camera."""
|
||||
from amcrest import AmcrestCamera
|
||||
camera = AmcrestCamera(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
camera.current_time
|
||||
# pylint: disable=broad-except
|
||||
except Exception as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
device = discovery_info['device']
|
||||
authentication = discovery_info['authentication']
|
||||
ffmpeg_arguments = discovery_info['ffmpeg_arguments']
|
||||
name = discovery_info['name']
|
||||
resolution = discovery_info['resolution']
|
||||
stream_source = discovery_info['stream_source']
|
||||
|
||||
async_add_devices([
|
||||
AmcrestCam(hass,
|
||||
name,
|
||||
device,
|
||||
authentication,
|
||||
ffmpeg_arguments,
|
||||
stream_source,
|
||||
resolution)], True)
|
||||
|
||||
add_devices([AmcrestCam(hass, config, camera)])
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, device_info, camera):
|
||||
def __init__(self, hass, name, camera, authentication,
|
||||
ffmpeg_arguments, stream_source, resolution):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = name
|
||||
self._camera = camera
|
||||
self._base_url = self._camera.get_base_url()
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
|
||||
self._stream_source = STREAM_SOURCE_LIST[
|
||||
device_info.get(CONF_STREAM_SOURCE)
|
||||
]
|
||||
self._token = self._auth = aiohttp.BasicAuth(
|
||||
device_info.get(CONF_USERNAME),
|
||||
password=device_info.get(CONF_PASSWORD)
|
||||
)
|
||||
self._ffmpeg_arguments = ffmpeg_arguments
|
||||
self._stream_source = stream_source
|
||||
self._resolution = resolution
|
||||
self._token = self._auth = authentication
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
|
||||
@@ -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,14 @@ class ArloCam(Camera):
|
||||
def __init__(self, hass, camera, device_info):
|
||||
"""Initialize an Arlo camera."""
|
||||
super().__init__()
|
||||
|
||||
self._camera = camera
|
||||
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 +89,36 @@ 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."""
|
||||
# Get the list of base stations identified by library
|
||||
base_stations = self.hass.data[DATA_ARLO].base_stations
|
||||
|
||||
# Some Arlo cameras does not have basestation
|
||||
# So check if there is base station detected first
|
||||
# if yes, then choose the primary base station
|
||||
# Set the mode on the chosen base station
|
||||
if base_stations:
|
||||
primary_base_station = base_stations[0]
|
||||
primary_base_station.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."""
|
||||
|
||||
@@ -113,8 +113,7 @@ class NetatmoCamera(Camera):
|
||||
return "Presence"
|
||||
elif self._cameratype == "NACamera":
|
||||
return "Welcome"
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
||||
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'
|
||||
@@ -54,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error("Unable to connect to NVR: %s", str(ex))
|
||||
return False
|
||||
|
||||
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid'
|
||||
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
|
||||
# Filter out airCam models, which are not supported in the latest
|
||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||
cameras = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -398,16 +398,14 @@ class ClimateDevice(Entity):
|
||||
"""Return the current state."""
|
||||
if self.current_operation:
|
||||
return self.current_operation
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
if self.unit_of_measurement == TEMP_CELSIUS:
|
||||
return PRECISION_TENTHS
|
||||
else:
|
||||
return PRECISION_WHOLE
|
||||
return PRECISION_WHOLE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@@ -693,8 +691,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)
|
||||
@@ -703,6 +707,5 @@ class ClimateDevice(Entity):
|
||||
return round(temp * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(temp, 1)
|
||||
else:
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
|
||||
@@ -151,16 +151,14 @@ class Thermostat(ClimateDevice):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
@@ -171,8 +169,7 @@ class Thermostat(ClimateDevice):
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
elif self.current_operation == STATE_COOL:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def desired_fan_mode(self):
|
||||
@@ -184,8 +181,7 @@ class Thermostat(ClimateDevice):
|
||||
"""Return the current fan state."""
|
||||
if 'fan' in self.thermostat['equipmentStatus']:
|
||||
return STATE_ON
|
||||
else:
|
||||
return STATE_OFF
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
@@ -199,15 +195,13 @@ class Thermostat(ClimateDevice):
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# A temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
else:
|
||||
# A permanent hold from away climate is away_mode
|
||||
return None
|
||||
# A permanent hold from away climate is away_mode
|
||||
return None
|
||||
elif event['holdClimateRef'] != "":
|
||||
# Any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
else:
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
elif event['type'].startswith('auto'):
|
||||
# All auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
@@ -222,8 +216,7 @@ class Thermostat(ClimateDevice):
|
||||
if self.operation_mode == 'auxHeatOnly' or \
|
||||
self.operation_mode == 'heatPump':
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return self.operation_mode
|
||||
return self.operation_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@@ -384,8 +377,7 @@ class Thermostat(ClimateDevice):
|
||||
# add further conditions if other hold durations should be
|
||||
# supported; note that this should not include 'indefinite'
|
||||
# as an indefinite away hold is interpreted as away_mode
|
||||
else:
|
||||
return 'nextTransition'
|
||||
return 'nextTransition'
|
||||
|
||||
@property
|
||||
def climate_list(self):
|
||||
|
||||
@@ -145,4 +145,4 @@ class Flexit(ClimateDevice):
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new fan mode."""
|
||||
self.unit.set_fan_speed(fan)
|
||||
self.unit.set_fan_speed(self._fan_list.index(fan))
|
||||
|
||||
@@ -12,7 +12,8 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import switch
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
STATE_AUTO)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
|
||||
CONF_NAME)
|
||||
@@ -87,6 +88,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
self._keep_alive = keep_alive
|
||||
self._enabled = True
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@@ -131,18 +133,39 @@ class GenericThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self._enabled:
|
||||
return STATE_OFF
|
||||
if self.ac_mode:
|
||||
cooling = self._active and self._is_device_active
|
||||
return STATE_COOL if cooling else STATE_IDLE
|
||||
else:
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [STATE_AUTO, STATE_OFF]
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_AUTO:
|
||||
self._enabled = True
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
_LOGGER.error('Unrecognized operation mode: %s', operation_mode)
|
||||
return
|
||||
# Ensure we updae the current operation after changing the mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -159,9 +182,9 @@ class GenericThermostat(ClimateDevice):
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return ClimateDevice.min_temp.fget(self)
|
||||
|
||||
# get default temp from super class
|
||||
return ClimateDevice.min_temp.fget(self)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
@@ -169,9 +192,9 @@ class GenericThermostat(ClimateDevice):
|
||||
# pylint: disable=no-member
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
|
||||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_sensor_changed(self, entity_id, old_state, new_state):
|
||||
@@ -221,6 +244,9 @@ class GenericThermostat(ClimateDevice):
|
||||
if not self._active:
|
||||
return
|
||||
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if self.min_cycle_duration:
|
||||
if self._is_device_active:
|
||||
current_state = STATE_ON
|
||||
|
||||
@@ -46,8 +46,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if region == 'us':
|
||||
return _setup_us(username, password, config, add_devices)
|
||||
else:
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
|
||||
def _setup_round(username, password, config, add_devices):
|
||||
@@ -251,8 +251,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._device.system_mode == 'cool':
|
||||
return self._device.setpoint_cool
|
||||
else:
|
||||
return self._device.setpoint_heat
|
||||
return self._device.setpoint_heat
|
||||
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -109,16 +109,14 @@ class NestThermostat(ClimateDevice):
|
||||
return self._mode
|
||||
elif self._mode == STATE_HEAT_COOL:
|
||||
return STATE_AUTO
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on:
|
||||
return self._target_temperature
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@@ -129,8 +127,7 @@ class NestThermostat(ClimateDevice):
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
@@ -141,8 +138,7 @@ class NestThermostat(ClimateDevice):
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
@@ -188,9 +184,8 @@ class NestThermostat(ClimateDevice):
|
||||
if self._has_fan:
|
||||
# Return whether the fan is on
|
||||
return STATE_ON if self._fan else STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
||||
@@ -119,14 +119,14 @@ class NetatmoThermostat(ClimateDevice):
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = False
|
||||
|
||||
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature for 2 hours."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
mode = "manual"
|
||||
self._data.thermostatdata.setthermpoint(
|
||||
mode, temperature, endTimeOffset)
|
||||
mode, temperature, DEFAULT_TIME_OFFSET)
|
||||
self._target_temperature = temperature
|
||||
self._away = False
|
||||
|
||||
|
||||
@@ -92,8 +92,7 @@ class ThermostatDevice(ClimateDevice):
|
||||
"""Return current operation i.e. heat, cool, idle."""
|
||||
if self._state:
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return STATE_IDLE
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def current_temperature(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
|
||||
@@ -135,18 +136,54 @@ class RadioThermostat(ClimateDevice):
|
||||
return self._away
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
self._current_temperature = self.device.temp['raw']
|
||||
"""Update and validate the data from the thermostat."""
|
||||
current_temp = self.device.temp['raw']
|
||||
if current_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid temperature reading")
|
||||
return
|
||||
self._current_temperature = current_temp
|
||||
self._name = self.device.name['raw']
|
||||
self._fmode = self.device.fmode['human']
|
||||
self._tmode = self.device.tmode['human']
|
||||
try:
|
||||
self._fmode = self.device.fmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid fan mode reading")
|
||||
try:
|
||||
self._tmode = self.device.tmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat mode reading")
|
||||
try:
|
||||
self._tstate = self.device.tstate['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat state reading")
|
||||
|
||||
if self._tmode == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_COOL
|
||||
elif self._tmode == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_HEAT
|
||||
elif self._tmode == 'Auto':
|
||||
if self._tstate == 'Cool':
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
elif self._tstate == 'Heat':
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_AUTO
|
||||
else:
|
||||
self._current_operation = STATE_IDLE
|
||||
|
||||
@@ -159,6 +196,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)
|
||||
@@ -115,9 +117,8 @@ class SensiboClimate(ClimateDevice):
|
||||
# We are working in same units as the a/c unit. Use whole degrees
|
||||
# like the API supports.
|
||||
return 1
|
||||
else:
|
||||
# Unit conversion is going on. No point to stick to specific steps.
|
||||
return None
|
||||
# Unit conversion is going on. No point to stick to specific steps.
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
@@ -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',
|
||||
@@ -41,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
zones = tado.get_zones()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to get zone info from mytado")
|
||||
return False
|
||||
return
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
@@ -50,9 +61,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if climate_devices:
|
||||
add_devices(climate_devices, True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
@@ -60,9 +68,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 +121,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,6 +145,8 @@ class TadoClimate(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current readable operation mode."""
|
||||
if self._cooling:
|
||||
return "Cooling"
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
|
||||
@property
|
||||
@@ -136,6 +154,20 @@ class TadoClimate(ClimateDevice):
|
||||
"""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)
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
if self.ac_mode:
|
||||
return list(FAN_MODES_LIST.values())
|
||||
return None
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement used by the platform."""
|
||||
@@ -180,18 +212,16 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
@@ -205,27 +235,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 +265,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."""
|
||||
|
||||
@@ -111,8 +111,8 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
# This will address both possibilities
|
||||
if self.wink.current_humidity() < 1:
|
||||
return self.wink.current_humidity() * 100
|
||||
else:
|
||||
return self.wink.current_humidity()
|
||||
return self.wink.current_humidity()
|
||||
return None
|
||||
|
||||
@property
|
||||
def external_temperature(self):
|
||||
@@ -175,10 +175,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return self.wink.current_max_set_point()
|
||||
elif self.current_operation == STATE_HEAT:
|
||||
return self.wink.current_min_set_point()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@@ -206,8 +203,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return True
|
||||
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -270,9 +266,8 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return STATE_ON
|
||||
elif self.wink.current_fan_mode() == 'auto':
|
||||
return STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
@@ -451,8 +446,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
return SPEED_MEDIUM
|
||||
elif speed <= 1.0 and speed > 0.8:
|
||||
return SPEED_HIGH
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
||||
@@ -154,8 +154,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
return TEMP_CELSIUS
|
||||
elif self._unit == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
return self._unit
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
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)
|
||||
@@ -40,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
|
||||
|
||||
@@ -112,10 +112,7 @@ class CommandCover(CoverDevice):
|
||||
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
|
||||
return self.current_cover_position == 0
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
||||
@@ -79,8 +79,7 @@ class DemoCover(CoverDevice):
|
||||
"""Flag supported features."""
|
||||
if self._supported_features is not None:
|
||||
return self._supported_features
|
||||
else:
|
||||
return super().supported_features
|
||||
return super().supported_features
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
|
||||
@@ -159,8 +159,7 @@ class GaradgetCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state == STATE_CLOSED
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
@@ -52,10 +52,7 @@ class HMCover(HMDevice, CoverDevice):
|
||||
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
|
||||
return self.current_cover_position == 0
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
|
||||
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
|
||||
@@ -361,8 +361,7 @@ class MqttCover(CoverDevice):
|
||||
position_percentage = float(offset_position) / tilt_range * 100.0
|
||||
if self._tilt_invert:
|
||||
return 100 - position_percentage
|
||||
else:
|
||||
return position_percentage
|
||||
return position_percentage
|
||||
|
||||
def find_in_range_from_percent(self, percentage):
|
||||
"""
|
||||
|
||||
@@ -53,8 +53,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_DIMMER in self._values:
|
||||
return self._values.get(set_req.V_DIMMER) == 0
|
||||
else:
|
||||
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
||||
@@ -117,8 +117,7 @@ class OpenGarageCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state in [STATE_CLOSED, STATE_OPENING]
|
||||
return self._state in [STATE_CLOSED, STATE_OPENING]
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the cover."""
|
||||
|
||||
373
homeassistant/components/cover/template.py
Normal file
373
homeassistant/components/cover/template.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""
|
||||
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'
|
||||
CONF_OPEN_OR_CLOSE = 'open_or_close'
|
||||
|
||||
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Inclusive(OPEN_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(CLOSE_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(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.get(OPEN_ACTION)
|
||||
close_action = device_config.get(CLOSE_ACTION)
|
||||
stop_action = device_config.get(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
|
||||
|
||||
if position_action is None and open_action is None:
|
||||
_LOGGER.error('Must specify at least one of %s' or '%s',
|
||||
OPEN_ACTION, POSITION_ACTION)
|
||||
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 = None
|
||||
if open_action is not None:
|
||||
self._open_script = Script(hass, open_action)
|
||||
self._close_script = None
|
||||
if close_action is not None:
|
||||
self._close_script = Script(hass, close_action)
|
||||
self._stop_script = None
|
||||
if stop_action is not None:
|
||||
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
|
||||
|
||||
if self._stop_script is not None:
|
||||
supported_features |= SUPPORT_STOP
|
||||
|
||||
if self._position_script 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."""
|
||||
if self._open_script:
|
||||
self.hass.async_add_job(self._open_script.async_run())
|
||||
elif self._position_script:
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": 100}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
if self._close_script:
|
||||
self.hass.async_add_job(self._close_script.async_run())
|
||||
elif self._position_script:
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": 0}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
if self._stop_script:
|
||||
self.hass.async_add_job(self._stop_script.async_run())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Set cover position."""
|
||||
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."""
|
||||
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)
|
||||
@@ -53,10 +53,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
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
|
||||
return self.current_cover_position == 0
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
|
||||
@@ -29,20 +29,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""Representation of a Wink cover device."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the cover."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['cover'].append(self)
|
||||
|
||||
def close_cover(self):
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the shade."""
|
||||
self.wink.set_state(0)
|
||||
|
||||
def open_cover(self):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the shade."""
|
||||
self.wink.set_state(1)
|
||||
|
||||
|
||||
@@ -73,8 +73,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
return None
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
@@ -86,8 +85,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
return 0
|
||||
elif self._current_position >= 95:
|
||||
return 100
|
||||
else:
|
||||
return self._current_position
|
||||
return self._current_position
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -60,20 +60,11 @@ _LEASES_REGEX = re.compile(
|
||||
r'(?P<host>([^\s]+))')
|
||||
|
||||
# Command to get both 5GHz and 2.4GHz clients
|
||||
_WL_CMD = '{ wl -i eth2 assoclist & wl -i eth1 assoclist ; }'
|
||||
_WL_CMD = 'for dev in `nvram get wl_ifnames`; do wl -i $dev assoclist; done'
|
||||
_WL_REGEX = re.compile(
|
||||
r'\w+\s' +
|
||||
r'(?P<mac>(([0-9A-F]{2}[:-]){5}([0-9A-F]{2})))')
|
||||
|
||||
_ARP_CMD = 'arp -n'
|
||||
_ARP_REGEX = re.compile(
|
||||
r'.+\s' +
|
||||
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
|
||||
r'.+\s' +
|
||||
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
|
||||
r'\s' +
|
||||
r'.*')
|
||||
|
||||
_IP_NEIGH_CMD = 'ip neigh'
|
||||
_IP_NEIGH_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3}|'
|
||||
@@ -84,15 +75,6 @@ _IP_NEIGH_REGEX = re.compile(
|
||||
r'\s?(router)?'
|
||||
r'(?P<status>(\w+))')
|
||||
|
||||
_NVRAM_CMD = 'nvram get client_info_tmp'
|
||||
_NVRAM_REGEX = re.compile(
|
||||
r'.*>.*>' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||
r'>' +
|
||||
r'(?P<mac>(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' +
|
||||
r'>' +
|
||||
r'.*')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
@@ -102,7 +84,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases')
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(DeviceScanner):
|
||||
@@ -173,7 +155,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info('Checking ARP')
|
||||
_LOGGER.info('Checking Devices')
|
||||
data = self.get_asuswrt_data()
|
||||
if not data:
|
||||
return False
|
||||
@@ -182,7 +164,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
client['status'] == 'REACHABLE' or
|
||||
client['status'] == 'DELAY' or
|
||||
client['status'] == 'STALE' or
|
||||
client['status'] == 'IN_NVRAM']
|
||||
client['status'] == 'IN_ASSOCLIST']
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
@@ -204,41 +186,12 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
|
||||
host = ''
|
||||
|
||||
# match mac addresses to IP addresses in ARP table
|
||||
for arp in result.arp:
|
||||
if match.group('mac').lower() in \
|
||||
arp.decode('utf-8').lower():
|
||||
arp_match = _ARP_REGEX.search(
|
||||
arp.decode('utf-8').lower())
|
||||
if not arp_match:
|
||||
_LOGGER.warning("Could not parse arp row: %s", arp)
|
||||
continue
|
||||
|
||||
devices[arp_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': arp_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
# match mac addresses to IP addresses in NVRAM table
|
||||
for nvr in result.nvram:
|
||||
if match.group('mac').upper() in nvr.decode('utf-8'):
|
||||
nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8'))
|
||||
if not nvram_match:
|
||||
_LOGGER.warning("Could not parse nvr row: %s", nvr)
|
||||
continue
|
||||
|
||||
# skip current check if already in ARP table
|
||||
if nvram_match.group('ip') in devices.keys():
|
||||
continue
|
||||
|
||||
devices[nvram_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': 'IN_NVRAM',
|
||||
'ip': nvram_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
devices[match.group('mac').upper()] = {
|
||||
'host': host,
|
||||
'status': 'IN_ASSOCLIST',
|
||||
'ip': '',
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
else:
|
||||
for lease in result.leases:
|
||||
@@ -256,20 +209,23 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
devices[match.group('mac')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
for neighbor in result.neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
|
||||
continue
|
||||
if match.group('ip') in devices:
|
||||
devices[match.group('ip')]['status'] = match.group('status')
|
||||
for neighbor in result.neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s",
|
||||
neighbor)
|
||||
continue
|
||||
if match.group('mac') in devices:
|
||||
devices[match.group('mac')]['status'] = (
|
||||
match.group('status'))
|
||||
|
||||
return devices
|
||||
|
||||
|
||||
@@ -317,27 +273,19 @@ class SshConnection(_Connection):
|
||||
try:
|
||||
if not self.connected:
|
||||
self.connect()
|
||||
self._ssh.sendline(_IP_NEIGH_CMD)
|
||||
self._ssh.prompt()
|
||||
neighbors = self._ssh.before.split(b'\n')[1:-1]
|
||||
if self._ap:
|
||||
self._ssh.sendline(_ARP_CMD)
|
||||
self._ssh.prompt()
|
||||
arp_result = self._ssh.before.split(b'\n')[1:-1]
|
||||
neighbors = ['']
|
||||
self._ssh.sendline(_WL_CMD)
|
||||
self._ssh.prompt()
|
||||
leases_result = self._ssh.before.split(b'\n')[1:-1]
|
||||
self._ssh.sendline(_NVRAM_CMD)
|
||||
self._ssh.prompt()
|
||||
nvram_result = self._ssh.before.split(b'\n')[1].split(b'<')[1:]
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
self._ssh.sendline(_IP_NEIGH_CMD)
|
||||
self._ssh.prompt()
|
||||
neighbors = self._ssh.before.split(b'\n')[1:-1]
|
||||
self._ssh.sendline(_LEASES_CMD)
|
||||
self._ssh.prompt()
|
||||
leases_result = self._ssh.before.split(b'\n')[1:-1]
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
return AsusWrtResult(neighbors, leases_result)
|
||||
except exceptions.EOF as err:
|
||||
_LOGGER.error("Connection refused. SSH enabled?")
|
||||
self.disconnect()
|
||||
@@ -407,23 +355,14 @@ class TelnetConnection(_Connection):
|
||||
neighbors = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
if self._ap:
|
||||
self._telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
|
||||
arp_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||
leases_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
self._telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
|
||||
nvram_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1].split(b'<')[1:])
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
return AsusWrtResult(neighbors, leases_result)
|
||||
except EOFError:
|
||||
_LOGGER.error("Unexpected response from router")
|
||||
self.disconnect()
|
||||
|
||||
@@ -52,8 +52,7 @@ class BboxDeviceScanner(DeviceScanner):
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
||||
@@ -87,21 +87,20 @@ class CiscoDeviceScanner(DeviceScanner):
|
||||
lines_result = lines_result[2:]
|
||||
|
||||
for line in lines_result:
|
||||
if len(line.split()) is 6:
|
||||
parts = line.split()
|
||||
if len(parts) != 6:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) != 6:
|
||||
continue
|
||||
|
||||
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
|
||||
# 'GigabitEthernet0']
|
||||
age = parts[2]
|
||||
hw_addr = parts[3]
|
||||
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
|
||||
# 'GigabitEthernet0']
|
||||
age = parts[2]
|
||||
hw_addr = parts[3]
|
||||
|
||||
if age != "-":
|
||||
mac = _parse_cisco_mac_address(hw_addr)
|
||||
age = int(age)
|
||||
if age < 1:
|
||||
last_results.append(mac)
|
||||
if age != "-":
|
||||
mac = _parse_cisco_mac_address(hw_addr)
|
||||
age = int(age)
|
||||
if age < 1:
|
||||
last_results.append(mac)
|
||||
|
||||
self.last_results = last_results
|
||||
return True
|
||||
|
||||
113
homeassistant/components/device_tracker/linksys_smart.py
Normal file
113
homeassistant/components/device_tracker/linksys_smart.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""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 = None
|
||||
for prop in device["properties"]:
|
||||
if prop["name"] == "userDeviceName":
|
||||
name = prop["value"]
|
||||
if not name:
|
||||
name = device.get("friendlyName", device["deviceID"])
|
||||
|
||||
_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)
|
||||
@@ -94,21 +94,20 @@ class LocativeView(HomeAssistantView):
|
||||
partial(self.see, dev_id=device,
|
||||
location_name=location_name, gps=gps_location))
|
||||
return 'Setting location to not home'
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered
|
||||
# before the previous zone was exited. The enter message will
|
||||
# be sent first, then the exit message will be sent second.
|
||||
return 'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state)
|
||||
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered
|
||||
# before the previous zone was exited. The enter message will
|
||||
# be sent first, then the exit message will be sent second.
|
||||
return 'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state)
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
return 'Received test message.'
|
||||
|
||||
else:
|
||||
_LOGGER.error('Received unidentified message from Locative: %s',
|
||||
direction)
|
||||
return ('Received unidentified message: {}'.format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error('Received unidentified message from Locative: %s',
|
||||
direction)
|
||||
return ('Received unidentified message: {}'.format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,8 +95,7 @@ class NmapDeviceScanner(DeviceScanner):
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -131,8 +131,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
plaintext_payload = decrypt_payload(topic, data['data'])
|
||||
if plaintext_payload is None:
|
||||
return None
|
||||
else:
|
||||
return validate_payload(topic, plaintext_payload, data_type)
|
||||
return validate_payload(topic, plaintext_payload, data_type)
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != data_type:
|
||||
_LOGGER.debug("Skipping %s update for following data "
|
||||
|
||||
@@ -90,8 +90,7 @@ class TadoDeviceScanner(DeviceScanner):
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
||||
@@ -73,8 +73,8 @@ class TomatoDeviceScanner(DeviceScanner):
|
||||
|
||||
if not filter_named or not filter_named[0]:
|
||||
return None
|
||||
else:
|
||||
return filter_named[0]
|
||||
|
||||
return filter_named[0]
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_tomato_info(self):
|
||||
|
||||
@@ -9,7 +9,7 @@ import hashlib
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@@ -33,8 +33,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a TP-Link scanner."""
|
||||
for cls in [Tplink4DeviceScanner, Tplink3DeviceScanner,
|
||||
Tplink2DeviceScanner, TplinkDeviceScanner]:
|
||||
for cls in [Tplink5DeviceScanner, Tplink4DeviceScanner,
|
||||
Tplink3DeviceScanner, Tplink2DeviceScanner,
|
||||
TplinkDeviceScanner]:
|
||||
scanner = cls(config[DOMAIN])
|
||||
if scanner.success_init:
|
||||
return scanner
|
||||
@@ -234,10 +235,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
self.stok = ''
|
||||
self.sysauth = ''
|
||||
return False
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"An unknown error happened while fetching data")
|
||||
return False
|
||||
_LOGGER.error(
|
||||
"An unknown error happened while fetching data")
|
||||
return False
|
||||
except ValueError:
|
||||
_LOGGER.error("Router didn't respond with JSON. "
|
||||
"Check if credentials are correct")
|
||||
@@ -350,3 +350,83 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
|
||||
|
||||
self.last_results = [mac.replace("-", ":") for mac in mac_results]
|
||||
return True
|
||||
|
||||
|
||||
class Tplink5DeviceScanner(TplinkDeviceScanner):
|
||||
"""This class queries a TP-Link EAP-225 AP with newer TP-Link FW."""
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found MAC IDs."""
|
||||
self._update_info()
|
||||
return self.last_results.keys()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""Get firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the TP-Link AP is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
with self.lock:
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
base_url = 'http://{}'.format(self.host)
|
||||
|
||||
header = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
|
||||
" rv:53.0) Gecko/20100101 Firefox/53.0",
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Language": "Accept-Language: en-US,en;q=0.5",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Content-Type": "application/x-www-form-urlencoded; "
|
||||
"charset=UTF-8",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Referer": "http://" + self.host + "/",
|
||||
"Connection": "keep-alive",
|
||||
"Pragma": "no-cache",
|
||||
"Cache-Control": "no-cache"
|
||||
}
|
||||
|
||||
password_md5 = hashlib.md5(self.password).hexdigest().upper()
|
||||
|
||||
# create a session to handle cookie easier
|
||||
session = requests.session()
|
||||
session.get(base_url, headers=header)
|
||||
|
||||
login_data = {"username": self.username, "password": password_md5}
|
||||
session.post(base_url, login_data, headers=header)
|
||||
|
||||
# a timestamp is required to be sent as get parameter
|
||||
timestamp = int(datetime.now().timestamp() * 1e3)
|
||||
|
||||
client_list_url = '{}/data/monitor.client.client.json'.format(
|
||||
base_url)
|
||||
|
||||
get_params = {
|
||||
'operation': 'load',
|
||||
'_': timestamp
|
||||
}
|
||||
|
||||
response = session.get(client_list_url,
|
||||
headers=header,
|
||||
params=get_params)
|
||||
session.close()
|
||||
try:
|
||||
list_of_devices = response.json()
|
||||
except ValueError:
|
||||
_LOGGER.error("AP didn't respond with JSON. "
|
||||
"Check if credentials are correct.")
|
||||
return False
|
||||
|
||||
if list_of_devices:
|
||||
self.last_results = {
|
||||
device['MAC'].replace('-', ':'): device['DeviceName']
|
||||
for device in list_of_devices['data']
|
||||
}
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
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:
|
||||
|
||||
@@ -12,11 +12,10 @@ import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
|
||||
@@ -25,12 +24,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_IP = '192.168.0.1'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string,
|
||||
})
|
||||
|
||||
CMD_LOGIN = 15
|
||||
CMD_LOGOUT = 16
|
||||
CMD_DEVICES = 123
|
||||
|
||||
|
||||
@@ -38,7 +34,7 @@ CMD_DEVICES = 123
|
||||
def async_get_scanner(hass, config):
|
||||
"""Return the UPC device scanner."""
|
||||
scanner = UPCDeviceScanner(hass, config[DOMAIN])
|
||||
success_init = yield from scanner.async_login()
|
||||
success_init = yield from scanner.async_initialize_token()
|
||||
|
||||
return scanner if success_init else None
|
||||
|
||||
@@ -50,7 +46,6 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
"""Initialize the scanner."""
|
||||
self.hass = hass
|
||||
self.host = config[CONF_HOST]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.data = {}
|
||||
self.token = None
|
||||
@@ -65,21 +60,12 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
|
||||
self.websession = async_get_clientsession(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_logout(event):
|
||||
"""Logout from upc connect box."""
|
||||
yield from self._async_ws_function(CMD_LOGOUT)
|
||||
self.token = None
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, async_logout)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
if self.token is None:
|
||||
reconnect = yield from self.async_login()
|
||||
if not reconnect:
|
||||
token_initialized = yield from self.async_initialize_token()
|
||||
if not token_initialized:
|
||||
_LOGGER.error("Not connected to %s", self.host)
|
||||
return []
|
||||
|
||||
@@ -95,55 +81,42 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_device_name(self, device):
|
||||
"""Ge the firmware doesn't save the name of the wireless device."""
|
||||
"""The firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_login(self):
|
||||
"""Login into firmware and get first token."""
|
||||
def async_initialize_token(self):
|
||||
"""Get first token."""
|
||||
try:
|
||||
# get first token
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.websession.get(
|
||||
"http://{}/common_page/login.html".format(self.host)
|
||||
"http://{}/common_page/login.html".format(self.host),
|
||||
headers=self.headers
|
||||
)
|
||||
|
||||
yield from response.text()
|
||||
|
||||
self.token = response.cookies['sessionToken'].value
|
||||
|
||||
# login
|
||||
data = yield from self._async_ws_function(CMD_LOGIN, {
|
||||
'Username': 'NULL',
|
||||
'Password': self.password,
|
||||
})
|
||||
|
||||
# Successful?
|
||||
return data is not None
|
||||
return True
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Can not load login page from %s", self.host)
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_ws_function(self, function, additional_form=None):
|
||||
def _async_ws_function(self, function):
|
||||
"""Execute a command on UPC firmware webservice."""
|
||||
form_data = {
|
||||
'token': self.token,
|
||||
'fun': function
|
||||
}
|
||||
|
||||
if additional_form:
|
||||
form_data.update(additional_form)
|
||||
|
||||
redirects = function != CMD_DEVICES
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
# The 'token' parameter has to be first, and 'fun' second
|
||||
# or the UPC firmware will return an error
|
||||
response = yield from self.websession.post(
|
||||
"http://{}/xml/getter.xml".format(self.host),
|
||||
data=form_data,
|
||||
data="token={}&fun={}".format(self.token, function),
|
||||
headers=self.headers,
|
||||
allow_redirects=redirects
|
||||
allow_redirects=False
|
||||
)
|
||||
|
||||
# error?
|
||||
|
||||
@@ -101,10 +101,10 @@ class XiaomiDeviceScanner(DeviceScanner):
|
||||
result = _retrieve_list(self.host, self.token)
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
_LOGGER.info("Refreshing token and retrying device list refresh")
|
||||
self.token = _get_token(self.host, self.username, self.password)
|
||||
return _retrieve_list(self.host, self.token)
|
||||
|
||||
_LOGGER.info("Refreshing token and retrying device list refresh")
|
||||
self.token = _get_token(self.host, self.username, self.password)
|
||||
return _retrieve_list(self.host, self.token)
|
||||
|
||||
def _store_result(self, result):
|
||||
"""Extract and store the device list in self.last_results."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ SERVICE_HASS_IOS_APP = 'hass_ios'
|
||||
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
|
||||
SERVICE_HASSIO = 'hassio'
|
||||
SERVICE_AXIS = 'axis'
|
||||
SERVICE_APPLE_TV = 'apple_tv'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
@@ -40,6 +41,7 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_IKEA_TRADFRI: ('tradfri', None),
|
||||
SERVICE_HASSIO: ('hassio', None),
|
||||
SERVICE_AXIS: ('axis', None),
|
||||
SERVICE_APPLE_TV: ('apple_tv', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
@@ -52,9 +54,9 @@ SERVICE_HANDLERS = {
|
||||
'denonavr': ('media_player', 'denonavr'),
|
||||
'samsung_tv': ('media_player', 'samsungtv'),
|
||||
'yeelight': ('light', 'yeelight'),
|
||||
'apple_tv': ('media_player', 'apple_tv'),
|
||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
'harmony': ('remote', 'harmony'),
|
||||
'bose_soundtouch': ('media_player', 'soundtouch'),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Parent component for Dyson Pure Cool Link devices."""
|
||||
"""Parent component for Dyson Pure Cool Link devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/dyson/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -9,7 +13,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \
|
||||
CONF_DEVICES
|
||||
|
||||
REQUIREMENTS = ['libpurecoollink==0.1.5']
|
||||
REQUIREMENTS = ['libpurecoollink==0.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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()
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Support for Dyson Pure Cool link fan."""
|
||||
"""Support for Dyson Pure Cool link fan.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.dyson/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
from os import path
|
||||
@@ -75,13 +79,15 @@ class DysonPureCoolLinkDevice(FanEntity):
|
||||
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))
|
||||
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()
|
||||
from libpurecoollink.dyson import DysonState
|
||||
if isinstance(message, DysonState):
|
||||
_LOGGER.debug("Message received for fan device %s : %s", self.name,
|
||||
message)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -157,8 +163,7 @@ class DysonPureCoolLinkDevice(FanEntity):
|
||||
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 int(self._device.state.speed)
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
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 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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user