Compare commits
376 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc442361cd | |||
| c3ff5de016 | |||
| 9b15cfa5a5 | |||
| 7837b4893f | |||
| d88edb0661 | |||
| 7a50450b92 | |||
| 3afa4726bf | |||
| 218e97d965 | |||
| 8d19192c9c | |||
| 179d99381d | |||
| b8dfa4c3d2 | |||
| 92bec562ab | |||
| 1802c0a922 | |||
| f2cc00cc64 | |||
| 033e06868e | |||
| 4883036789 | |||
| 965bd8a2e0 | |||
| 2224d05607 | |||
| ed66c21035 | |||
| 9384e2c963 | |||
| 6af42b4372 | |||
| 1235dae5b8 | |||
| a35d05ac7e | |||
| 6b103cc9b9 | |||
| a23f60315f | |||
| 3bc6a7da32 | |||
| dc5b610ee5 | |||
| 7723db9e62 | |||
| cf612c3d5b | |||
| d2b6660881 | |||
| 3972d1d4c6 | |||
| 5fda78cf91 | |||
| 60ce2b343d | |||
| 6cc285aea5 | |||
| c5d5d57e9b | |||
| 02979db3d6 | |||
| 0d06e8c1c9 | |||
| d793cfeb68 | |||
| 382a62346b | |||
| 9e0ca719ed | |||
| 88b70e964c | |||
| 4dda842b16 | |||
| 3cba09c6f6 | |||
| c4bc42d527 | |||
| cf04a81f70 | |||
| 92014bf1d1 | |||
| cba55402b1 | |||
| f56b3d8e9c | |||
| 8313225b40 | |||
| 13042d5557 | |||
| 85858885c8 | |||
| 10f48fbf6b | |||
| 8b267e3faf | |||
| 13effee679 | |||
| 903cda08b1 | |||
| e0e2f739ba | |||
| 0e710099e0 | |||
| b1b0a2589e | |||
| dff36b8087 | |||
| 46ad194097 | |||
| 2ba9d825a0 | |||
| 8c0035c5b3 | |||
| c53fc94e84 | |||
| bccd880395 | |||
| 8267a21bfe | |||
| 3cbd77f6ac | |||
| 4a6b5ba02b | |||
| efb83dde19 | |||
| c20324793e | |||
| 4496ee5af0 | |||
| e42c4859c2 | |||
| 939d1b5ff6 | |||
| 5c2cecde70 | |||
| c613585100 | |||
| 3fbf09e7d9 | |||
| b22a26891e | |||
| a0df165011 | |||
| f6307a1523 | |||
| ff9688bb7a | |||
| c714658384 | |||
| f21914d1f3 | |||
| 417c193c0d | |||
| 455c629f47 | |||
| 01896786fb | |||
| c526fcd40f | |||
| 8b57777ce9 | |||
| b61197196e | |||
| e0a1b87296 | |||
| d3ed6d5242 | |||
| 5d6455415e | |||
| a27b37be59 | |||
| f6a67d99e4 | |||
| 486263fff7 | |||
| 5a469f4d4b | |||
| 889eef78e4 | |||
| 794cfb7976 | |||
| 9055922153 | |||
| 2330268842 | |||
| e81d17dae0 | |||
| 5820bab9b1 | |||
| 3a00c3de64 | |||
| 71fb7a6ef6 | |||
| 65d841c3a6 | |||
| 5236b720ab | |||
| ebbdce1f42 | |||
| 1490ccf7fb | |||
| 642e9c53ba | |||
| 1e9e6927eb | |||
| b7c7041873 | |||
| 35f35050ff | |||
| 6cb02128b4 | |||
| 3436676de2 | |||
| 04de22613c | |||
| 2cf5acdfd2 | |||
| 4512a972d1 | |||
| 65df6f0013 | |||
| eb00e54eba | |||
| ec700c2cd6 | |||
| 75a39352ff | |||
| 36c7fbe06a | |||
| bb2191b2b0 | |||
| f314b6cb6c | |||
| 86e1d0f952 | |||
| 02c3ea1917 | |||
| 0a67195529 | |||
| feb70b47df | |||
| 909f613324 | |||
| f0bf7b0def | |||
| 541707c3e7 | |||
| add89d6973 | |||
| e2cec9b3ae | |||
| 0f914b4c20 | |||
| 1b4be0460c | |||
| cb899a9465 | |||
| b362017206 | |||
| 6e63a4ed8a | |||
| b9c852392c | |||
| 976a0fe38c | |||
| fc8b25a71f | |||
| fcbf7abdaa | |||
| 7759ab6919 | |||
| 3fd620198e | |||
| 49bc95549b | |||
| ba0f7a4101 | |||
| 4914ad1dd9 | |||
| f07a4684e0 | |||
| cfd78f7b02 | |||
| 2a2e6b6334 | |||
| b635637541 | |||
| a7ebba6863 | |||
| 1f8acb49bc | |||
| 5a4bca9780 | |||
| d7e52d8014 | |||
| 391a8901c8 | |||
| b98e03b5bc | |||
| 6b586c268a | |||
| 9a34e7174c | |||
| 3203849b60 | |||
| 2e582a4597 | |||
| 5d38eec37d | |||
| a6c7fe04da | |||
| 966ab20f26 | |||
| b2e9dc5c8f | |||
| fc3a8e4e79 | |||
| 00352d41a7 | |||
| 29c26e0015 | |||
| 63d9bd4a9c | |||
| cb23549af6 | |||
| e5cc5a58e1 | |||
| aa8db784d5 | |||
| d68d4d1129 | |||
| 8d32e883bd | |||
| 7826b9aa72 | |||
| e91d47db96 | |||
| e92e433805 | |||
| 40e1d35268 | |||
| af6c39f4d1 | |||
| 5be949f00f | |||
| 169459b57f | |||
| 05926b1994 | |||
| 54b4142530 | |||
| f0244d7982 | |||
| 7a600ea064 | |||
| d687bc073e | |||
| a59b02b6b4 | |||
| 14919082a3 | |||
| 802a95eac5 | |||
| a44181fd35 | |||
| b280a791a6 | |||
| 7269070d97 | |||
| 94ac0b5ed8 | |||
| 8ef8dbc868 | |||
| 7de3c62846 | |||
| fb90dab471 | |||
| 419ec7f7a7 | |||
| bdf64ccbbb | |||
| 3fa45375d9 | |||
| 5566ea8c81 | |||
| 8c303bf48c | |||
| 8683d75aa1 | |||
| 4f5d7cea11 | |||
| ab9ffc4f05 | |||
| 6e2bfcfe65 | |||
| 240098dd7e | |||
| 353bb62687 | |||
| 46df91ff45 | |||
| 295caeb065 | |||
| eeb309aea1 | |||
| 9e0a765801 | |||
| c94cc34a8f | |||
| 901d4b5489 | |||
| 2e4e3a42cc | |||
| b866687cd7 | |||
| 7faa94046c | |||
| 1d579587c1 | |||
| b28bfad496 | |||
| e0682044f0 | |||
| 81f1a65fae | |||
| 8efc4b5ba9 | |||
| b4e2537de3 | |||
| 90e25a6dfb | |||
| 3d90855ca6 | |||
| 200c927087 | |||
| ef22a6e18d | |||
| 061395d2f8 | |||
| 05258ea4bf | |||
| 8742ce035a | |||
| 0664bf31a2 | |||
| 0ec1ff642d | |||
| cd44824233 | |||
| 79240a0d47 | |||
| c03d04d826 | |||
| dfb8b5a3c1 | |||
| 432304be82 | |||
| 294d8171a2 | |||
| a46ddcf6dd | |||
| 5ca006cc9c | |||
| 564ed26aeb | |||
| 5860267410 | |||
| ec9638f4d1 | |||
| 7db8bbf385 | |||
| 024f1d4882 | |||
| 3375261f51 | |||
| 640d58f0a8 | |||
| 793b8b8ad3 | |||
| 39af43eb5c | |||
| b56675a7bb | |||
| c4d71e934d | |||
| f4d7bbd044 | |||
| 3d5d90241f | |||
| a7c8e202aa | |||
| a63658d583 | |||
| ca81180e6d | |||
| de4c8adca2 | |||
| 823e260c2a | |||
| 1c8b5838cd | |||
| 3473ef63af | |||
| 2cced1dac3 | |||
| b5d3a4736b | |||
| e627544479 | |||
| d547345f90 | |||
| 4ec3289f9c | |||
| 638dd25aff | |||
| 37efd5a5cd | |||
| 168065b9bc | |||
| 95cd2035b6 | |||
| aeba81e193 | |||
| c7e327ea87 | |||
| ed06b8cead | |||
| a79c7ee217 | |||
| 6bf23f9167 | |||
| 1b3963299d | |||
| c461a7c7e2 | |||
| 0cfff13be1 | |||
| 0245189670 | |||
| 7777d5811f | |||
| 7259cc878e | |||
| a4214afddb | |||
| b078f6c342 | |||
| 81974885ee | |||
| b2c5a9f5fe | |||
| 04cb893d10 | |||
| 3b228c78c0 | |||
| 4e91e6d103 | |||
| cb4e886a4f | |||
| bee80c5b79 | |||
| 4479761131 | |||
| 20f1e1609f | |||
| 1f1115f631 | |||
| f7c2ec19ef | |||
| fed7bd9473 | |||
| 4d6070e33a | |||
| 0a7e6ac222 | |||
| f892c3394b | |||
| 929d49ed6f | |||
| 3c1f8cd882 | |||
| f21da7cfdc | |||
| 39d33c97ff | |||
| 3193e825d5 | |||
| c262a387dc | |||
| 36d5fff8e0 | |||
| 72aa722c33 | |||
| c952f2e18a | |||
| 0fc7f37185 | |||
| 9cff6c7e6a | |||
| e66268dffe | |||
| c13b510ba3 | |||
| 5f4baa67dc | |||
| 1db7e2c9d6 | |||
| fa324dce9c | |||
| 56c694b477 | |||
| 3f764f1981 | |||
| 63d6734612 | |||
| f9743c29cd | |||
| bdb7a29586 | |||
| 22c36f0ad3 | |||
| fd6373c7aa | |||
| 87fe674c70 | |||
| ddec566e10 | |||
| 454d8535f8 | |||
| 8e4942088e | |||
| 3af527b1b5 | |||
| 69d5738e47 | |||
| 379c10985b | |||
| 821cf7135d | |||
| d986bdab98 | |||
| 53d9fd18b7 | |||
| 38a1f06d14 | |||
| 2e2d0f48fb | |||
| 4652b8aea1 | |||
| ef1cbd3aea | |||
| 31cedf83c7 | |||
| 19a97580fc | |||
| 17f3cf0389 | |||
| 4e02300cbc | |||
| 015cdd155c | |||
| d4e603cc6a | |||
| 7ae374e11f | |||
| 292b403dc3 | |||
| b815898ddb | |||
| b1855f1d1d | |||
| bd6a17a3a5 | |||
| 29fad3fa3c | |||
| 0c43466225 | |||
| 0d6c95ac44 | |||
| 6776e942d7 | |||
| 879e32f670 | |||
| 3a246df544 | |||
| 9577525b0b | |||
| 6b410d8076 | |||
| 9e82433a3e | |||
| 8ceaa72ba3 | |||
| fce994ea76 | |||
| 4390fed168 | |||
| 0f8e48c26d | |||
| 2d556486bf | |||
| 29f47d58bc | |||
| 8947052405 | |||
| 475b7896e2 | |||
| b2a2cb3fd8 | |||
| a9feafd571 | |||
| a3c6211c04 | |||
| afcb0b8767 | |||
| c0c439c549 | |||
| 959f6386b4 | |||
| f1fe8e95ba | |||
| 6892033556 | |||
| 766893253a | |||
| ef5edb95ba | |||
| f21b9988e9 | |||
| 7859b76429 | |||
| 2c44e4fb12 | |||
| 9b373901fa | |||
| 37be81c20c | |||
| 5fe2db228c | |||
| c91d52a587 |
+41
-13
@@ -11,6 +11,9 @@ omit =
|
||||
homeassistant/components/abode.py
|
||||
homeassistant/components/*/abode.py
|
||||
|
||||
homeassistant/components/ads/__init__.py
|
||||
homeassistant/components/*/ads.py
|
||||
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
@@ -47,9 +50,15 @@ omit =
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
homeassistant/components/coinbase.py
|
||||
homeassistant/components/sensor/coinbase.py
|
||||
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
homeassistant/components/deconz/*
|
||||
homeassistant/components/*/deconz.py
|
||||
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
@@ -85,7 +94,7 @@ omit =
|
||||
homeassistant/components/hive.py
|
||||
homeassistant/components/*/hive.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/homematic/__init__.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
@@ -260,9 +269,14 @@ omit =
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/daikin.py
|
||||
homeassistant/components/*/daikin.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/egardia.py
|
||||
homeassistant/components/alarm_control_panel/ialarm.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
@@ -279,26 +293,31 @@ omit =
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/calendar/caldav.py
|
||||
homeassistant/components/calendar/todoist.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/canary.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/ring.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/camera/yi.py
|
||||
homeassistant/components/climate/econet.py
|
||||
homeassistant/components/climate/ephember.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/flexit.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/honeywell.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/climate/sensibo.py
|
||||
homeassistant/components/climate/touchline.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/knx.py
|
||||
@@ -331,10 +350,10 @@ omit =
|
||||
homeassistant/components/device_tracker/sky_hub.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tado.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tile.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
@@ -352,20 +371,22 @@ omit =
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/light/avion.py
|
||||
homeassistant/components/light/blinkt.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/blinkt.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/decora_wifi.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/greenwave.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/iglo.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
homeassistant/components/light/sensehat.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/tplink.py
|
||||
@@ -376,9 +397,9 @@ omit =
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/nello.py
|
||||
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
|
||||
@@ -420,11 +441,13 @@ omit =
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/ue_smart_radio.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
|
||||
homeassistant/components/media_player/yamaha_musiccast.py
|
||||
homeassistant/components/media_player/ziggo_mediabox_xl.py
|
||||
homeassistant/components/mycroft.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
@@ -466,12 +489,14 @@ omit =
|
||||
homeassistant/components/notify/yessssms.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/prometheus.py
|
||||
homeassistant/components/rainbird.py
|
||||
homeassistant/components/remember_the_milk/__init__.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/airvisual.py
|
||||
homeassistant/components/sensor/alpha_vantage.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
@@ -482,8 +507,8 @@ omit =
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/crimereports.py
|
||||
@@ -493,6 +518,7 @@ omit =
|
||||
homeassistant/components/sensor/deluge.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/discogs.py
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
@@ -506,11 +532,11 @@ omit =
|
||||
homeassistant/components/sensor/etherscan.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/fido.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gearbest.py
|
||||
homeassistant/components/sensor/geizhals.py
|
||||
homeassistant/components/sensor/gitter.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
@@ -520,7 +546,6 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/hydroquebec.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
@@ -558,6 +583,7 @@ omit =
|
||||
homeassistant/components/sensor/pyload.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
@@ -568,6 +594,7 @@ omit =
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
homeassistant/components/sensor/sma.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sochain.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
@@ -621,8 +648,8 @@ omit =
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/snmp.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/xiaomi_miio.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
@@ -631,16 +658,17 @@ omit =
|
||||
homeassistant/components/tts/baidu.py
|
||||
homeassistant/components/tts/microsoft.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
homeassistant/components/vacuum/roomba.py
|
||||
homeassistant/components/vacuum/xiaomi_miio.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
homeassistant/components/weather/darksky.py
|
||||
homeassistant/components/weather/metoffice.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/yweather.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/util.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Ensure Docker script files uses LF to support Docker for Windows.
|
||||
setup_docker_prereqs eol=lf
|
||||
/virtualization/Docker/scripts/* eol=lf
|
||||
@@ -11,6 +11,7 @@
|
||||
```
|
||||
|
||||
## Checklist:
|
||||
- [ ] The code change is tested and works locally.
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
|
||||
@@ -74,6 +74,7 @@ pip-selfcheck.json
|
||||
venv
|
||||
.venv
|
||||
Pipfile*
|
||||
share/*
|
||||
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
|
||||
+5
-1
@@ -53,9 +53,11 @@ homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/plant.py @ChristianKuehnel
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
@@ -63,9 +65,11 @@ homeassistant/components/switch/rainmachine.py @bachya
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
homeassistant/components/*/axis.py @kane610
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
|
||||
@@ -230,7 +230,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
||||
|
||||
def cmdline() -> List[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if sys.argv[0].endswith(os.path.sep + '__main__.py'):
|
||||
if os.path.basename(sys.argv[0]) == '__main__.py':
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
|
||||
@@ -10,7 +10,6 @@ Component design guidelines:
|
||||
import asyncio
|
||||
import itertools as it
|
||||
import logging
|
||||
import os
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
@@ -111,11 +110,6 @@ def async_reload_core_config(hass):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up general services related to Home Assistant."""
|
||||
descriptions = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_turn_service(service):
|
||||
"""Handle calls to homeassistant.turn_on/off."""
|
||||
@@ -155,14 +149,11 @@ def async_setup(hass, config):
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TURN_OFF])
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TURN_ON])
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TOGGLE])
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_core_service(call):
|
||||
@@ -187,14 +178,11 @@ def async_setup(hass, config):
|
||||
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_STOP])
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_RESTART])
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_CHECK_CONFIG])
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_reload_config(call):
|
||||
@@ -209,7 +197,6 @@ def async_setup(hass, config):
|
||||
hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config,
|
||||
descriptions[ha.DOMAIN][SERVICE_RELOAD_CORE_CONFIG])
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -7,11 +7,9 @@ https://home-assistant.io/components/abode/
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import partial
|
||||
from os import path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME,
|
||||
CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS,
|
||||
@@ -188,22 +186,16 @@ def setup_hass_services(hass):
|
||||
for device in target_devices:
|
||||
device.trigger()
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))[DOMAIN]
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SETTINGS, change_setting,
|
||||
descriptions.get(SERVICE_SETTINGS),
|
||||
schema=CHANGE_SETTING_SCHEMA)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image,
|
||||
descriptions.get(SERVICE_CAPTURE_IMAGE),
|
||||
schema=CAPTURE_IMAGE_SCHEMA)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_quick_action,
|
||||
descriptions.get(SERVICE_TRIGGER),
|
||||
schema=TRIGGER_SCHEMA)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
ADS Component.
|
||||
|
||||
For more details about this component, please refer to the documentation.
|
||||
https://home-assistant.io/components/ads/
|
||||
|
||||
"""
|
||||
import threading
|
||||
import struct
|
||||
import logging
|
||||
import ctypes
|
||||
from collections import namedtuple
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
|
||||
EVENT_HOMEASSISTANT_STOP
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyads==2.2.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_ADS = 'data_ads'
|
||||
|
||||
# Supported Types
|
||||
ADSTYPE_INT = 'int'
|
||||
ADSTYPE_UINT = 'uint'
|
||||
ADSTYPE_BYTE = 'byte'
|
||||
ADSTYPE_BOOL = 'bool'
|
||||
|
||||
DOMAIN = 'ads'
|
||||
|
||||
# config variable names
|
||||
CONF_ADS_VAR = 'adsvar'
|
||||
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
|
||||
CONF_ADS_TYPE = 'adstype'
|
||||
CONF_ADS_FACTOR = 'factor'
|
||||
CONF_ADS_VALUE = 'value'
|
||||
|
||||
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_IP_ADDRESS): cv.string,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT,
|
||||
ADSTYPE_BYTE]),
|
||||
vol.Required(CONF_ADS_VALUE): cv.match_all
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the ADS component."""
|
||||
import pyads
|
||||
conf = config[DOMAIN]
|
||||
|
||||
# get ads connection parameters from config
|
||||
net_id = conf.get(CONF_DEVICE)
|
||||
ip_address = conf.get(CONF_IP_ADDRESS)
|
||||
port = conf.get(CONF_PORT)
|
||||
|
||||
# create a new ads connection
|
||||
client = pyads.Connection(net_id, port, ip_address)
|
||||
|
||||
# add some constants to AdsHub
|
||||
AdsHub.ADS_TYPEMAP = {
|
||||
ADSTYPE_BOOL: pyads.PLCTYPE_BOOL,
|
||||
ADSTYPE_BYTE: pyads.PLCTYPE_BYTE,
|
||||
ADSTYPE_INT: pyads.PLCTYPE_INT,
|
||||
ADSTYPE_UINT: pyads.PLCTYPE_UINT,
|
||||
}
|
||||
|
||||
AdsHub.PLCTYPE_BOOL = pyads.PLCTYPE_BOOL
|
||||
AdsHub.PLCTYPE_BYTE = pyads.PLCTYPE_BYTE
|
||||
AdsHub.PLCTYPE_INT = pyads.PLCTYPE_INT
|
||||
AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT
|
||||
AdsHub.ADSError = pyads.ADSError
|
||||
|
||||
# connect to ads client and try to connect
|
||||
try:
|
||||
ads = AdsHub(client)
|
||||
except pyads.pyads.ADSError:
|
||||
_LOGGER.error(
|
||||
'Could not connect to ADS host (netid=%s, port=%s)', net_id, port
|
||||
)
|
||||
return False
|
||||
|
||||
# add ads hub to hass data collection, listen to shutdown
|
||||
hass.data[DATA_ADS] = ads
|
||||
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, ads.shutdown)
|
||||
|
||||
def handle_write_data_by_name(call):
|
||||
"""Write a value to the connected ADS device."""
|
||||
ads_var = call.data.get(CONF_ADS_VAR)
|
||||
ads_type = call.data.get(CONF_ADS_TYPE)
|
||||
value = call.data.get(CONF_ADS_VALUE)
|
||||
|
||||
try:
|
||||
ads.write_by_name(ads_var, value, ads.ADS_TYPEMAP[ads_type])
|
||||
except pyads.ADSError as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
|
||||
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# tuple to hold data needed for notification
|
||||
NotificationItem = namedtuple(
|
||||
'NotificationItem', 'hnotify huser name plc_datatype callback'
|
||||
)
|
||||
|
||||
|
||||
class AdsHub:
|
||||
"""Representation of a PyADS connection."""
|
||||
|
||||
def __init__(self, ads_client):
|
||||
"""Initialize the ADS Hub."""
|
||||
self._client = ads_client
|
||||
self._client.open()
|
||||
|
||||
# all ADS devices are registered here
|
||||
self._devices = []
|
||||
self._notification_items = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def shutdown(self, *args, **kwargs):
|
||||
"""Shutdown ADS connection."""
|
||||
_LOGGER.debug('Shutting down ADS')
|
||||
for notification_item in self._notification_items.values():
|
||||
self._client.del_device_notification(
|
||||
notification_item.hnotify,
|
||||
notification_item.huser
|
||||
)
|
||||
_LOGGER.debug(
|
||||
'Deleting device notification %d, %d',
|
||||
notification_item.hnotify, notification_item.huser
|
||||
)
|
||||
self._client.close()
|
||||
|
||||
def register_device(self, device):
|
||||
"""Register a new device."""
|
||||
self._devices.append(device)
|
||||
|
||||
def write_by_name(self, name, value, plc_datatype):
|
||||
"""Write a value to the device."""
|
||||
with self._lock:
|
||||
return self._client.write_by_name(name, value, plc_datatype)
|
||||
|
||||
def read_by_name(self, name, plc_datatype):
|
||||
"""Read a value from the device."""
|
||||
with self._lock:
|
||||
return self._client.read_by_name(name, plc_datatype)
|
||||
|
||||
def add_device_notification(self, name, plc_datatype, callback):
|
||||
"""Add a notification to the ADS devices."""
|
||||
from pyads import NotificationAttrib
|
||||
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
|
||||
|
||||
with self._lock:
|
||||
hnotify, huser = self._client.add_device_notification(
|
||||
name, attr, self._device_notification_callback
|
||||
)
|
||||
hnotify = int(hnotify)
|
||||
|
||||
_LOGGER.debug(
|
||||
'Added Device Notification %d for variable %s', hnotify, name
|
||||
)
|
||||
|
||||
self._notification_items[hnotify] = NotificationItem(
|
||||
hnotify, huser, name, plc_datatype, callback
|
||||
)
|
||||
|
||||
def _device_notification_callback(self, addr, notification, huser):
|
||||
"""Handle device notifications."""
|
||||
contents = notification.contents
|
||||
|
||||
hnotify = int(contents.hNotification)
|
||||
_LOGGER.debug('Received Notification %d', hnotify)
|
||||
data = contents.data
|
||||
|
||||
try:
|
||||
notification_item = self._notification_items[hnotify]
|
||||
except KeyError:
|
||||
_LOGGER.debug('Unknown Device Notification handle: %d', hnotify)
|
||||
return
|
||||
|
||||
# parse data to desired datatype
|
||||
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
|
||||
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_INT:
|
||||
value = struct.unpack('<h', bytearray(data)[:2])[0]
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
|
||||
value = struct.unpack('<B', bytearray(data)[:1])[0]
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
|
||||
value = struct.unpack('<H', bytearray(data)[:2])[0]
|
||||
else:
|
||||
value = bytearray(data)
|
||||
_LOGGER.warning('No callback available for this datatype.')
|
||||
|
||||
# execute callback
|
||||
notification_item.callback(notification_item.name, value)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Describes the format for available ADS services
|
||||
|
||||
write_data_by_name:
|
||||
description: Write a value to the connected ADS device.
|
||||
|
||||
fields:
|
||||
adsvar:
|
||||
description: The name of the variable to write to.
|
||||
example: '.global_var'
|
||||
adstype:
|
||||
description: The data type of the variable to write to.
|
||||
example: 'int'
|
||||
value:
|
||||
description: The value to write to the variable.
|
||||
example: 1
|
||||
@@ -7,7 +7,6 @@ https://home-assistant.io/components/alarm_control_panel/
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -15,7 +14,6 @@ from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -148,14 +146,10 @@ def async_setup(hass, config):
|
||||
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'))
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_alarm_service_handler,
|
||||
descriptions.get(service), schema=ALARM_SERVICE_SCHEMA)
|
||||
schema=ALARM_SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -7,69 +7,86 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.components.alarmdecoder import (DATA_AD,
|
||||
SIGNAL_PANEL_MESSAGE)
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.alarmdecoder import (
|
||||
DATA_AD, SIGNAL_PANEL_MESSAGE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime'
|
||||
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up for AlarmDecoder alarm panels."""
|
||||
_LOGGER.debug("AlarmDecoderAlarmPanel: setup")
|
||||
device = AlarmDecoderAlarmPanel()
|
||||
add_devices([device])
|
||||
|
||||
device = AlarmDecoderAlarmPanel("Alarm Panel", hass)
|
||||
def alarm_toggle_chime_handler(service):
|
||||
"""Register toggle chime handler."""
|
||||
code = service.data.get(ATTR_CODE)
|
||||
device.alarm_toggle_chime(code)
|
||||
|
||||
async_add_devices([device])
|
||||
|
||||
return True
|
||||
hass.services.register(
|
||||
alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler,
|
||||
schema=ALARM_TOGGLE_CHIME_SCHEMA)
|
||||
|
||||
|
||||
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
def __init__(self, name, hass):
|
||||
def __init__(self):
|
||||
"""Initialize the alarm panel."""
|
||||
self._display = ""
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
_LOGGER.debug("Setting up panel")
|
||||
self._name = "Alarm Panel"
|
||||
self._state = None
|
||||
self._ac_power = None
|
||||
self._backlight_on = None
|
||||
self._battery_low = None
|
||||
self._check_zone = None
|
||||
self._chime = None
|
||||
self._entry_delay_off = None
|
||||
self._programming_mode = None
|
||||
self._ready = None
|
||||
self._zone_bypassed = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
|
||||
@callback
|
||||
def _message_callback(self, message):
|
||||
if message.alarm_sounding or message.fire_alarm:
|
||||
if self._state != STATE_ALARM_TRIGGERED:
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self.async_schedule_update_ha_state()
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
elif message.armed_away:
|
||||
if self._state != STATE_ALARM_ARMED_AWAY:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self.async_schedule_update_ha_state()
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif message.armed_home:
|
||||
if self._state != STATE_ALARM_ARMED_HOME:
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self.async_schedule_update_ha_state()
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
if self._state != STATE_ALARM_DISARMED:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self.async_schedule_update_ha_state()
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
|
||||
self._ac_power = message.ac_power
|
||||
self._backlight_on = message.backlight_on
|
||||
self._battery_low = message.battery_low
|
||||
self._check_zone = message.check_zone
|
||||
self._chime = message.chime_on
|
||||
self._entry_delay_off = message.entry_delay_off
|
||||
self._programming_mode = message.programming_mode
|
||||
self._ready = message.ready
|
||||
self._zone_bypassed = message.zone_bypassed
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -91,26 +108,37 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'ac_power': self._ac_power,
|
||||
'backlight_on': self._backlight_on,
|
||||
'battery_low': self._battery_low,
|
||||
'check_zone': self._check_zone,
|
||||
'chime': self._chime,
|
||||
'entry_delay_off': self._entry_delay_off,
|
||||
'programming_mode': self._programming_mode,
|
||||
'ready': self._ready,
|
||||
'zone_bypassed': self._zone_bypassed
|
||||
}
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
_LOGGER.debug("alarm_disarm: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("alarm_disarm: sending %s1", str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}1".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
_LOGGER.debug("alarm_arm_away: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("alarm_arm_away: sending %s2", str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}2".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
_LOGGER.debug("alarm_arm_home: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("alarm_arm_home: sending %s3", str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}3".format(code))
|
||||
|
||||
def alarm_toggle_chime(self, code=None):
|
||||
"""Send toggle chime command."""
|
||||
if code:
|
||||
self.hass.data[DATA_AD].send("{!s}9".format(code))
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Support for Canary alarm.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.canary/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.canary import DATA_CANARY
|
||||
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
|
||||
|
||||
DEPENDENCIES = ['canary']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Canary alarms."""
|
||||
data = hass.data[DATA_CANARY]
|
||||
devices = []
|
||||
|
||||
for location in data.locations:
|
||||
devices.append(CanaryAlarm(data, location.location_id))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class CanaryAlarm(AlarmControlPanel):
|
||||
"""Representation of a Canary alarm control panel."""
|
||||
|
||||
def __init__(self, data, location_id):
|
||||
"""Initialize a Canary security camera."""
|
||||
self._data = data
|
||||
self._location_id = location_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alarm."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
return location.name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
|
||||
LOCATION_MODE_NIGHT
|
||||
|
||||
location = self._data.get_location(self._location_id)
|
||||
|
||||
if location.is_private:
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
mode = location.mode
|
||||
if mode.name == LOCATION_MODE_AWAY:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif mode.name == LOCATION_MODE_HOME:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode.name == LOCATION_MODE_NIGHT:
|
||||
return STATE_ALARM_ARMED_NIGHT
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
return {
|
||||
'private': location.is_private
|
||||
}
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
self._data.set_location_mode(self._location_id, location.mode.name,
|
||||
True)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from canary.api import LOCATION_MODE_HOME
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
from canary.api import LOCATION_MODE_AWAY
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
from canary.api import LOCATION_MODE_NIGHT
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
|
||||
Executable → Regular
+2
-2
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
REQUIREMENTS = ['concord232==0.15']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -121,4 +121,4 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('auto')
|
||||
self._alarm.arm('away')
|
||||
|
||||
@@ -4,30 +4,45 @@ Demo platform that has two fake alarm control panels.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import datetime
|
||||
import homeassistant.components.alarm_control_panel.manual as manual
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED, CONF_PENDING_TIME)
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME,
|
||||
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo alarm control panel platform."""
|
||||
add_devices([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False, {
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
|
||||
STATE_ALARM_ARMED_AWAY: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_HOME: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_NIGHT: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_DISARMED: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_TRIGGERED: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -16,9 +16,9 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN,
|
||||
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.22']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.26']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,13 +26,15 @@ CONF_REPORT_SERVER_CODES = 'report_server_codes'
|
||||
CONF_REPORT_SERVER_ENABLED = 'report_server_enabled'
|
||||
CONF_REPORT_SERVER_PORT = 'report_server_port'
|
||||
CONF_REPORT_SERVER_CODES_IGNORE = 'ignore'
|
||||
CONF_VERSION = 'version'
|
||||
|
||||
DEFAULT_NAME = 'Egardia'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_REPORT_SERVER_ENABLED = False
|
||||
DEFAULT_REPORT_SERVER_PORT = 52010
|
||||
DEFAULT_VERSION = 'GATE-01'
|
||||
DOMAIN = 'egardia'
|
||||
|
||||
D_EGARDIASRV = 'egardiaserver'
|
||||
NOTIFICATION_ID = 'egardia_notification'
|
||||
NOTIFICATION_TITLE = 'Egardia'
|
||||
|
||||
@@ -49,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_REPORT_SERVER_CODES): vol.All(cv.ensure_list),
|
||||
@@ -62,6 +65,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Egardia platform."""
|
||||
from pythonegardia import egardiadevice
|
||||
from pythonegardia import egardiaserver
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
@@ -71,41 +75,62 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
rs_enabled = config.get(CONF_REPORT_SERVER_ENABLED)
|
||||
rs_port = config.get(CONF_REPORT_SERVER_PORT)
|
||||
rs_codes = config.get(CONF_REPORT_SERVER_CODES)
|
||||
version = config.get(CONF_VERSION)
|
||||
|
||||
try:
|
||||
egardiasystem = egardiadevice.EgardiaDevice(
|
||||
host, port, username, password, '')
|
||||
host, port, username, password, '', version)
|
||||
except requests.exceptions.RequestException:
|
||||
raise exc.PlatformNotReady()
|
||||
except egardiadevice.UnauthorizedError:
|
||||
_LOGGER.error("Unable to authorize. Wrong password or username")
|
||||
return False
|
||||
return
|
||||
|
||||
add_devices([EgardiaAlarm(
|
||||
name, egardiasystem, hass, rs_enabled, rs_port, rs_codes)], True)
|
||||
eg_dev = EgardiaAlarm(
|
||||
name, egardiasystem, rs_enabled, rs_codes)
|
||||
|
||||
if rs_enabled:
|
||||
# Set up the egardia server
|
||||
_LOGGER.info("Setting up EgardiaServer")
|
||||
try:
|
||||
if D_EGARDIASRV not in hass.data:
|
||||
server = egardiaserver.EgardiaServer('', rs_port)
|
||||
bound = server.bind()
|
||||
if not bound:
|
||||
raise IOError("Binding error occurred while " +
|
||||
"starting EgardiaServer")
|
||||
hass.data[D_EGARDIASRV] = server
|
||||
server.start()
|
||||
except IOError:
|
||||
return
|
||||
hass.data[D_EGARDIASRV].register_callback(eg_dev.handle_status_event)
|
||||
|
||||
def handle_stop_event(event):
|
||||
"""Callback function for HA stop event."""
|
||||
hass.data[D_EGARDIASRV].stop()
|
||||
|
||||
# listen to home assistant stop event
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event)
|
||||
|
||||
# add egardia alarm device
|
||||
add_devices([eg_dev], True)
|
||||
|
||||
|
||||
class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a Egardia alarm."""
|
||||
|
||||
def __init__(self, name, egardiasystem, hass, rs_enabled=False,
|
||||
rs_port=None, rs_codes=None):
|
||||
def __init__(self, name, egardiasystem,
|
||||
rs_enabled=False, rs_codes=None):
|
||||
"""Initialize object."""
|
||||
self._name = name
|
||||
self._egardiasystem = egardiasystem
|
||||
self._status = STATE_UNKNOWN
|
||||
self._status = None
|
||||
self._rs_enabled = rs_enabled
|
||||
self._rs_port = rs_port
|
||||
self._hass = hass
|
||||
|
||||
if rs_codes is not None:
|
||||
self._rs_codes = rs_codes[0]
|
||||
else:
|
||||
self._rs_codes = rs_codes
|
||||
|
||||
if self._rs_enabled:
|
||||
self.listen_to_system_status()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
@@ -116,17 +141,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the state of the device."""
|
||||
return self._status
|
||||
|
||||
def handle_system_status_event(self, event):
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll if no report server is enabled."""
|
||||
if not self._rs_enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
def handle_status_event(self, event):
|
||||
"""Handle egardia_system_status_event."""
|
||||
if event.data.get('status') is not None:
|
||||
statuscode = event.data.get('status')
|
||||
statuscode = event.get('status')
|
||||
if statuscode is not None:
|
||||
status = self.lookupstatusfromcode(statuscode)
|
||||
self.parsestatus(status)
|
||||
|
||||
def listen_to_system_status(self):
|
||||
"""Subscribe to egardia_system_status event."""
|
||||
self._hass.bus.listen(
|
||||
'egardia_system_status', self.handle_system_status_event)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def lookupstatusfromcode(self, statuscode):
|
||||
"""Look at the rs_codes and returns the status from the code."""
|
||||
@@ -161,9 +189,8 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update the alarm status."""
|
||||
if not self._rs_enabled:
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -6,7 +6,6 @@ https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -14,7 +13,6 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.components.envisalink import (
|
||||
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
|
||||
@@ -69,14 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
for device in target_devices:
|
||||
device.async_alarm_keypress(keypress)
|
||||
|
||||
# Register Envisalink specific services
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler,
|
||||
descriptions.get(SERVICE_ALARM_KEYPRESS), schema=ALARM_KEYPRESS_SCHEMA)
|
||||
schema=ALARM_KEYPRESS_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Interfaces with iAlarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.ialarm/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['pyialarm==0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'iAlarm'
|
||||
|
||||
|
||||
def no_application_protocol(value):
|
||||
"""Validate that value is without the application protocol."""
|
||||
protocol_separator = "://"
|
||||
if not value or protocol_separator in value:
|
||||
raise vol.Invalid(
|
||||
'Invalid host, {} is not allowed'.format(protocol_separator))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up an iAlarm control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
url = 'http://{}'.format(host)
|
||||
ialarm = IAlarmPanel(name, username, password, url)
|
||||
add_devices([ialarm], True)
|
||||
|
||||
|
||||
class IAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Represent an iAlarm status."""
|
||||
|
||||
def __init__(self, name, username, password, url):
|
||||
"""Initialize the iAlarm status."""
|
||||
from pyialarm import IAlarm
|
||||
|
||||
self._name = name
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._url = url
|
||||
self._state = None
|
||||
self._client = IAlarm(username, password, url)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
status = self._client.get_status()
|
||||
_LOGGER.debug('iAlarm status: %s', status)
|
||||
if status:
|
||||
status = int(status)
|
||||
|
||||
if status == self._client.DISARMED:
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status == self._client.ARMED_AWAY:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
state = None
|
||||
|
||||
self._state = state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._client.disarm()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._client.arm_away()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._client.arm_stay()
|
||||
@@ -16,24 +16,40 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_DISARMED, STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, CONF_CODE,
|
||||
CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
||||
CONF_DELAY_TIME, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
|
||||
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS]
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PRETRIGGER_STATES:
|
||||
if CONF_DELAY_TIME not in config[state]:
|
||||
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
|
||||
if CONF_TRIGGER_TIME not in config[state]:
|
||||
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
@@ -41,28 +57,44 @@ def _state_validator(config):
|
||||
return config
|
||||
|
||||
|
||||
STATE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
def _state_schema(state):
|
||||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All({
|
||||
vol.Required(CONF_PLATFORM): 'manual',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
}, _state_validator))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -74,8 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
|
||||
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
|
||||
config.get(CONF_CODE_TEMPLATE),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config
|
||||
)])
|
||||
@@ -86,27 +117,37 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for 'trigger_time'. After that will be
|
||||
triggered for 'trigger_time', after that we return to the previous state
|
||||
or disarm if `disarm_after_trigger` is true.
|
||||
When triggered, will be pending for the triggering state's 'delay_time'
|
||||
plus the triggered state's 'pending_time'.
|
||||
After that will be triggered for 'trigger_time', after that we return to
|
||||
the previous state or disarm if `disarm_after_trigger` is true.
|
||||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time, trigger_time,
|
||||
def __init__(self, hass, name, code, code_template,
|
||||
disarm_after_trigger, config):
|
||||
"""Init the manual alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
if code_template:
|
||||
self._code = code_template
|
||||
self._code.hass = hass
|
||||
else:
|
||||
self._code = code or None
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._pre_trigger_state = self._state
|
||||
self._previous_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._pending_time_by_state = {}
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
self._pending_time_by_state[state] = datetime.timedelta(
|
||||
seconds=config[state][CONF_PENDING_TIME])
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -121,15 +162,16 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state == STATE_ALARM_TRIGGERED:
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time_by_state[self._state] +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
self._state = self._pre_trigger_state
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
@@ -138,9 +180,21 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
return self._state
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
@property
|
||||
def _active_state(self):
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
else:
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
return self._state_ts + pending_time > dt_util.utcnow()
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
pending_time += self._delay_time_by_state[self._previous_state]
|
||||
return pending_time
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
@@ -185,26 +239,35 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
"""
|
||||
Send alarm trigger command.
|
||||
|
||||
No code needed, a trigger time of zero for the current state
|
||||
disables the alarm.
|
||||
"""
|
||||
if not self._trigger_time_by_state[self._active_state]:
|
||||
return
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
if self._state == state:
|
||||
return
|
||||
|
||||
self._previous_state = self._state
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
|
||||
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._trigger_time + pending_time)
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
@@ -212,7 +275,14 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if self._code is None:
|
||||
return True
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
@@ -223,6 +293,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
@@ -16,8 +16,8 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER)
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
||||
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
@@ -26,28 +26,44 @@ from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
|
||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
||||
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
||||
DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
|
||||
DEFAULT_DISARM = 'DISARM'
|
||||
|
||||
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED]
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PRETRIGGER_STATES:
|
||||
if CONF_DELAY_TIME not in config[state]:
|
||||
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
|
||||
if CONF_TRIGGER_TIME not in config[state]:
|
||||
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
@@ -55,27 +71,44 @@ def _state_validator(config):
|
||||
return config
|
||||
|
||||
|
||||
STATE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
def _state_schema(state):
|
||||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PLATFORM): 'manual_mqtt',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
@@ -93,8 +126,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
|
||||
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
|
||||
config.get(CONF_CODE_TEMPLATE),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config.get(mqtt.CONF_STATE_TOPIC),
|
||||
config.get(mqtt.CONF_COMMAND_TOPIC),
|
||||
@@ -111,13 +143,15 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for 'trigger_time'. After that will be
|
||||
triggered for 'trigger_time', after that we return to the previous state
|
||||
or disarm if `disarm_after_trigger` is true.
|
||||
When triggered, will be pending for the triggering state's 'delay_time'
|
||||
plus the triggered state's 'pending_time'.
|
||||
After that will be triggered for 'trigger_time', after that we return to
|
||||
the previous state or disarm if `disarm_after_trigger` is true.
|
||||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time,
|
||||
trigger_time, disarm_after_trigger,
|
||||
def __init__(self, hass, name, code, code_template,
|
||||
disarm_after_trigger,
|
||||
state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away,
|
||||
payload_arm_night, config):
|
||||
@@ -125,17 +159,24 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._pending_time = datetime.timedelta(seconds=pending_time)
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
if code_template:
|
||||
self._code = code_template
|
||||
self._code.hass = hass
|
||||
else:
|
||||
self._code = code or None
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._pre_trigger_state = self._state
|
||||
self._previous_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._pending_time_by_state = {}
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
self._pending_time_by_state[state] = datetime.timedelta(
|
||||
seconds=config[state][CONF_PENDING_TIME])
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
@@ -158,15 +199,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state == STATE_ALARM_TRIGGERED:
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time_by_state[self._state] +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
self._state = self._pre_trigger_state
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
@@ -175,9 +217,21 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
return self._state
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
@property
|
||||
def _active_state(self):
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
else:
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
return self._state_ts + pending_time > dt_util.utcnow()
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
pending_time += self._delay_time_by_state[self._previous_state]
|
||||
return pending_time
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
@@ -215,26 +269,35 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
self._update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
"""
|
||||
Send alarm trigger command.
|
||||
|
||||
No code needed, a trigger time of zero for the current state
|
||||
disables the alarm.
|
||||
"""
|
||||
if not self._trigger_time_by_state[self._active_state]:
|
||||
return
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
if self._state == state:
|
||||
return
|
||||
|
||||
self._previous_state = self._state
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
|
||||
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._trigger_time + pending_time)
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
@@ -242,7 +305,14 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if self._code is None:
|
||||
return True
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
@@ -253,6 +323,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
@@ -17,7 +17,9 @@ from homeassistant.const import (
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
||||
CONF_NAME, CONF_CODE)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS)
|
||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -54,15 +56,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||
config.get(CONF_CODE))])
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE))])
|
||||
|
||||
|
||||
class MqttAlarm(alarm.AlarmControlPanel):
|
||||
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, code):
|
||||
payload_arm_home, payload_arm_away, code, availability_topic,
|
||||
payload_available, payload_not_available):
|
||||
"""Init the MQTT Alarm Control Panel."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
self._state = STATE_UNKNOWN
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
@@ -73,11 +81,11 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._code = code
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
"""Subscribe mqtt events."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Run when new MQTT message has been received."""
|
||||
@@ -89,7 +97,7 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
self._state = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
|
||||
@@ -59,3 +59,13 @@ envisalink_alarm_keypress:
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel (1-6 characters).'
|
||||
example: '*71'
|
||||
|
||||
alarmdecoder_alarm_toggle_chime:
|
||||
description: Send the alarm the toggle chime command.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: A required code to toggle the alarm control panel chime with.
|
||||
example: 1234
|
||||
|
||||
@@ -14,7 +14,9 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.16']
|
||||
|
||||
@@ -76,6 +78,8 @@ class TotalConnect(alarm.AlarmControlPanel):
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY_NIGHT:
|
||||
state = STATE_ALARM_ARMED_NIGHT
|
||||
elif status == self._client.ARMED_CUSTOM_BYPASS:
|
||||
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||
elif status == self._client.ARMING:
|
||||
state = STATE_ALARM_ARMING
|
||||
elif status == self._client.DISARMING:
|
||||
|
||||
@@ -4,18 +4,18 @@ Support for AlarmDecoder devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
||||
|
||||
REQUIREMENTS = ['alarmdecoder==0.12.3']
|
||||
REQUIREMENTS = ['alarmdecoder==1.13.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,6 +32,7 @@ CONF_DEVICE_TYPE = 'type'
|
||||
CONF_PANEL_DISPLAY = 'panel_display'
|
||||
CONF_ZONE_NAME = 'name'
|
||||
CONF_ZONE_TYPE = 'type'
|
||||
CONF_ZONE_RFID = 'rfid'
|
||||
CONF_ZONES = 'zones'
|
||||
|
||||
DEFAULT_DEVICE_TYPE = 'socket'
|
||||
@@ -51,6 +52,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
|
||||
|
||||
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
||||
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
||||
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
|
||||
|
||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
||||
@@ -67,13 +69,15 @@ DEVICE_USB_SCHEMA = vol.Schema({
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONE_NAME): cv.string,
|
||||
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string})
|
||||
vol.Optional(CONF_ZONE_TYPE,
|
||||
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
|
||||
vol.Optional(CONF_ZONE_RFID): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA,
|
||||
DEVICE_SERIAL_SCHEMA,
|
||||
DEVICE_USB_SCHEMA),
|
||||
vol.Required(CONF_DEVICE): vol.Any(
|
||||
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA,
|
||||
DEVICE_USB_SCHEMA),
|
||||
vol.Optional(CONF_PANEL_DISPLAY,
|
||||
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
@@ -81,14 +85,14 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
def setup(hass, config):
|
||||
"""Set up for the AlarmDecoder devices."""
|
||||
from alarmdecoder import AlarmDecoder
|
||||
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
|
||||
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
restart = False
|
||||
device = conf.get(CONF_DEVICE)
|
||||
display = conf.get(CONF_PANEL_DISPLAY)
|
||||
zones = conf.get(CONF_ZONES)
|
||||
@@ -99,32 +103,55 @@ def async_setup(hass, config):
|
||||
path = DEFAULT_DEVICE_PATH
|
||||
baud = DEFAULT_DEVICE_BAUD
|
||||
|
||||
sync_connect = asyncio.Future(loop=hass.loop)
|
||||
|
||||
def handle_open(device):
|
||||
"""Handle the successful connection."""
|
||||
_LOGGER.info("Established a connection with the alarmdecoder")
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
||||
sync_connect.set_result(True)
|
||||
|
||||
@callback
|
||||
def stop_alarmdecoder(event):
|
||||
"""Handle the shutdown of AlarmDecoder."""
|
||||
_LOGGER.debug("Shutting down alarmdecoder")
|
||||
nonlocal restart
|
||||
restart = False
|
||||
controller.close()
|
||||
|
||||
@callback
|
||||
def open_connection(now=None):
|
||||
"""Open a connection to AlarmDecoder."""
|
||||
from alarmdecoder.util import NoDeviceError
|
||||
nonlocal restart
|
||||
try:
|
||||
controller.open(baud)
|
||||
except NoDeviceError:
|
||||
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
|
||||
hass.helpers.event.track_point_in_time(
|
||||
open_connection, dt_util.utcnow() + timedelta(seconds=5))
|
||||
return
|
||||
_LOGGER.debug("Established a connection with the alarmdecoder")
|
||||
restart = True
|
||||
|
||||
def handle_closed_connection(event):
|
||||
"""Restart after unexpected loss of connection."""
|
||||
nonlocal restart
|
||||
if not restart:
|
||||
return
|
||||
restart = False
|
||||
_LOGGER.warning("AlarmDecoder unexpectedly lost connection.")
|
||||
hass.add_job(open_connection)
|
||||
|
||||
def handle_message(sender, message):
|
||||
"""Handle message from AlarmDecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message)
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_PANEL_MESSAGE, message)
|
||||
|
||||
def handle_rfx_message(sender, message):
|
||||
"""Handle RFX message from AlarmDecoder."""
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_RFX_MESSAGE, message)
|
||||
|
||||
def zone_fault_callback(sender, zone):
|
||||
"""Handle zone fault from AlarmDecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone)
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_FAULT, zone)
|
||||
|
||||
def zone_restore_callback(sender, zone):
|
||||
"""Handle zone restore from AlarmDecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone)
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_RESTORE, zone)
|
||||
|
||||
controller = False
|
||||
if device_type == 'socket':
|
||||
@@ -139,30 +166,25 @@ def async_setup(hass, config):
|
||||
AlarmDecoder(USBDevice.find())
|
||||
return False
|
||||
|
||||
controller.on_open += handle_open
|
||||
controller.on_message += handle_message
|
||||
controller.on_rfx_message += handle_rfx_message
|
||||
controller.on_zone_fault += zone_fault_callback
|
||||
controller.on_zone_restore += zone_restore_callback
|
||||
controller.on_close += handle_closed_connection
|
||||
|
||||
hass.data[DATA_AD] = controller
|
||||
|
||||
controller.open(baud)
|
||||
open_connection()
|
||||
|
||||
result = yield from sync_connect
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
hass.async_add_job(
|
||||
async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf,
|
||||
config))
|
||||
load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
|
||||
|
||||
if zones:
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config))
|
||||
load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)
|
||||
|
||||
if display:
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'sensor', DOMAIN, conf, config))
|
||||
load_platform(hass, 'sensor', DOMAIN, conf, config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -7,12 +7,10 @@ https://home-assistant.io/components/alert/
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
@@ -129,22 +127,16 @@ def async_setup(hass, config):
|
||||
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
|
||||
all_alerts[entity.entity_id] = entity
|
||||
|
||||
# Read descriptions
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = descriptions.get(DOMAIN, {})
|
||||
|
||||
# Setup service calls
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA)
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TURN_ON), schema=ALERT_SERVICE_SCHEMA)
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TOGGLE), schema=ALERT_SERVICE_SCHEMA)
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
|
||||
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
|
||||
if tasks:
|
||||
|
||||
@@ -9,15 +9,16 @@ import asyncio
|
||||
import enum
|
||||
import logging
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.components import http
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from .const import DOMAIN, SYN_RESOLUTION_MATCH
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
|
||||
HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -47,6 +48,10 @@ def async_setup(hass):
|
||||
hass.http.register_view(AlexaIntentsView)
|
||||
|
||||
|
||||
class UnknownRequest(HomeAssistantError):
|
||||
"""When an unknown Alexa request is passed in."""
|
||||
|
||||
|
||||
class AlexaIntentsView(http.HomeAssistantView):
|
||||
"""Handle Alexa requests."""
|
||||
|
||||
@@ -57,71 +62,112 @@ class AlexaIntentsView(http.HomeAssistantView):
|
||||
def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
hass = request.app['hass']
|
||||
data = yield from request.json()
|
||||
message = yield from request.json()
|
||||
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
|
||||
req = data.get('request')
|
||||
|
||||
if req is None:
|
||||
_LOGGER.error('Received invalid data from Alexa: %s', data)
|
||||
return self.json_message('Expected request value not received',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
req_type = req['type']
|
||||
|
||||
if req_type == 'SessionEndedRequest':
|
||||
return None
|
||||
|
||||
alexa_intent_info = req.get('intent')
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
|
||||
if req_type != 'IntentRequest' and req_type != 'LaunchRequest':
|
||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
||||
return self.json_message(
|
||||
'Received unsupported request: {}'.format(req_type),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
if req_type == 'LaunchRequest':
|
||||
intent_name = data.get('session', {}) \
|
||||
.get('application', {}) \
|
||||
.get('applicationId')
|
||||
else:
|
||||
intent_name = alexa_intent_info['name']
|
||||
_LOGGER.debug('Received Alexa request: %s', message)
|
||||
|
||||
try:
|
||||
intent_response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_name,
|
||||
{key: {'value': value} for key, value
|
||||
in alexa_response.variables.items()})
|
||||
response = yield from async_handle_message(hass, message)
|
||||
return b'' if response is None else self.json(response)
|
||||
except UnknownRequest as err:
|
||||
_LOGGER.warning(str(err))
|
||||
return self.json(intent_error_response(
|
||||
hass, message, str(err)))
|
||||
|
||||
except intent.UnknownIntent as err:
|
||||
_LOGGER.warning('Received unknown intent %s', intent_name)
|
||||
alexa_response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"This intent is not yet configured within Home Assistant.")
|
||||
return self.json(alexa_response)
|
||||
_LOGGER.warning(str(err))
|
||||
return self.json(intent_error_response(
|
||||
hass, message,
|
||||
"This intent is not yet configured within Home Assistant."))
|
||||
|
||||
except intent.InvalidSlotInfo as err:
|
||||
_LOGGER.error('Received invalid slot data from Alexa: %s', err)
|
||||
return self.json_message('Invalid slot data received',
|
||||
HTTP_BAD_REQUEST)
|
||||
except intent.IntentError:
|
||||
_LOGGER.exception('Error handling request for %s', intent_name)
|
||||
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
|
||||
return self.json(intent_error_response(
|
||||
hass, message,
|
||||
"Invalid slot information received for this intent."))
|
||||
|
||||
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
||||
if intent_speech in intent_response.speech:
|
||||
alexa_response.add_speech(
|
||||
alexa_speech,
|
||||
intent_response.speech[intent_speech]['speech'])
|
||||
break
|
||||
except intent.IntentError as err:
|
||||
_LOGGER.exception(str(err))
|
||||
return self.json(intent_error_response(
|
||||
hass, message, "Error handling intent."))
|
||||
|
||||
if 'simple' in intent_response.card:
|
||||
alexa_response.add_card(
|
||||
CardType.simple, intent_response.card['simple']['title'],
|
||||
intent_response.card['simple']['content'])
|
||||
|
||||
return self.json(alexa_response)
|
||||
def intent_error_response(hass, message, error):
|
||||
"""Return an Alexa response that will speak the error message."""
|
||||
alexa_intent_info = message.get('request').get('intent')
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
alexa_response.add_speech(SpeechType.plaintext, error)
|
||||
return alexa_response.as_dict()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, message):
|
||||
"""Handle an Alexa intent.
|
||||
|
||||
Raises:
|
||||
- UnknownRequest
|
||||
- intent.UnknownIntent
|
||||
- intent.InvalidSlotInfo
|
||||
- intent.IntentError
|
||||
"""
|
||||
req = message.get('request')
|
||||
req_type = req['type']
|
||||
|
||||
handler = HANDLERS.get(req_type)
|
||||
|
||||
if not handler:
|
||||
raise UnknownRequest('Received unknown request {}'.format(req_type))
|
||||
|
||||
return (yield from handler(hass, message))
|
||||
|
||||
|
||||
@HANDLERS.register('SessionEndedRequest')
|
||||
@asyncio.coroutine
|
||||
def async_handle_session_end(hass, message):
|
||||
"""Handle a session end request."""
|
||||
return None
|
||||
|
||||
|
||||
@HANDLERS.register('IntentRequest')
|
||||
@HANDLERS.register('LaunchRequest')
|
||||
@asyncio.coroutine
|
||||
def async_handle_intent(hass, message):
|
||||
"""Handle an intent request.
|
||||
|
||||
Raises:
|
||||
- intent.UnknownIntent
|
||||
- intent.InvalidSlotInfo
|
||||
- intent.IntentError
|
||||
"""
|
||||
req = message.get('request')
|
||||
alexa_intent_info = req.get('intent')
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
|
||||
if req['type'] == 'LaunchRequest':
|
||||
intent_name = message.get('session', {}) \
|
||||
.get('application', {}) \
|
||||
.get('applicationId')
|
||||
else:
|
||||
intent_name = alexa_intent_info['name']
|
||||
|
||||
intent_response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_name,
|
||||
{key: {'value': value} for key, value
|
||||
in alexa_response.variables.items()})
|
||||
|
||||
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
||||
if intent_speech in intent_response.speech:
|
||||
alexa_response.add_speech(
|
||||
alexa_speech,
|
||||
intent_response.speech[intent_speech]['speech'])
|
||||
break
|
||||
|
||||
if 'simple' in intent_response.card:
|
||||
alexa_response.add_card(
|
||||
CardType.simple, intent_response.card['simple']['title'],
|
||||
intent_response.card['simple']['content'])
|
||||
|
||||
return alexa_response.as_dict()
|
||||
|
||||
|
||||
def resolve_slot_synonyms(key, request):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Support for alexa Smart Home Skill API."""
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import math
|
||||
from uuid import uuid4
|
||||
@@ -27,10 +26,9 @@ API_EVENT = 'event'
|
||||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
|
||||
ATTR_ALEXA_DESCRIPTION = 'alexa_description'
|
||||
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories'
|
||||
ATTR_ALEXA_HIDDEN = 'alexa_hidden'
|
||||
ATTR_ALEXA_NAME = 'alexa_name'
|
||||
CONF_DESCRIPTION = 'description'
|
||||
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
||||
CONF_NAME = 'name'
|
||||
|
||||
|
||||
MAPPING_COMPONENT = {
|
||||
@@ -73,7 +71,13 @@ MAPPING_COMPONENT = {
|
||||
}
|
||||
|
||||
|
||||
Config = namedtuple('AlexaConfig', 'filter')
|
||||
class Config:
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
def __init__(self, should_expose, entity_config=None):
|
||||
"""Initialize the configuration."""
|
||||
self.should_expose = should_expose
|
||||
self.entity_config = entity_config or {}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -150,32 +154,28 @@ def async_api_discovery(hass, config, request):
|
||||
discovery_endpoints = []
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
if not config.filter(entity.entity_id):
|
||||
if not config.should_expose(entity.entity_id):
|
||||
_LOGGER.debug("Not exposing %s because filtered by config",
|
||||
entity.entity_id)
|
||||
continue
|
||||
|
||||
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
|
||||
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
|
||||
entity.entity_id)
|
||||
continue
|
||||
|
||||
class_data = MAPPING_COMPONENT.get(entity.domain)
|
||||
|
||||
if not class_data:
|
||||
continue
|
||||
|
||||
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name)
|
||||
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
|
||||
entity.entity_id)
|
||||
entity_conf = config.entity_config.get(entity.entity_id, {})
|
||||
|
||||
friendly_name = entity_conf.get(CONF_NAME, entity.name)
|
||||
description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id)
|
||||
|
||||
# Required description as per Amazon Scene docs
|
||||
if entity.domain == scene.DOMAIN:
|
||||
scene_fmt = '{} (Scene connected via Home Assistant)'
|
||||
description = scene_fmt.format(description)
|
||||
|
||||
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES
|
||||
display_categories = entity.attributes.get(cat_key, class_data[0])
|
||||
display_categories = entity_conf.get(CONF_DISPLAY_CATEGORIES,
|
||||
class_data[0])
|
||||
|
||||
endpoint = {
|
||||
'displayCategories': [display_categories],
|
||||
@@ -243,9 +243,13 @@ def async_api_turn_on(hass, config, request, entity):
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
service = SERVICE_TURN_ON
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
|
||||
yield from hass.services.async_call(domain, service, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -259,9 +263,13 @@ def async_api_turn_off(hass, config, request, entity):
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
service = SERVICE_TURN_OFF
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
|
||||
yield from hass.services.async_call(domain, service, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -276,7 +284,7 @@ def async_api_set_brightness(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -300,7 +308,7 @@ def async_api_adjust_brightness(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -321,14 +329,14 @@ def async_api_set_color(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
else:
|
||||
xyz = color_util.color_RGB_to_xy(*rgb)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
|
||||
light.ATTR_BRIGHTNESS: xyz[2],
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -343,7 +351,7 @@ def async_api_set_color_temperature(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_KELVIN: kelvin,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -361,7 +369,7 @@ def async_api_decrease_color_temp(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -379,7 +387,7 @@ def async_api_increase_color_temp(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -391,7 +399,7 @@ def async_api_activate(hass, config, request, entity):
|
||||
"""Process a activate request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -421,8 +429,8 @@ def async_api_set_percentage(hass, config, request, entity):
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = percentage
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -469,8 +477,8 @@ def async_api_adjust_percentage(hass, config, request, entity):
|
||||
|
||||
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -482,7 +490,7 @@ def async_api_lock(hass, config, request, entity):
|
||||
"""Process a lock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -495,7 +503,7 @@ def async_api_unlock(hass, config, request, entity):
|
||||
"""Process a unlock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -512,8 +520,9 @@ def async_api_set_volume(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -540,9 +549,9 @@ def async_api_adjust_volume(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -559,9 +568,9 @@ def async_api_set_mute(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_MUTE,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_MUTE,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -575,8 +584,9 @@ def async_api_play(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -590,8 +600,9 @@ def async_api_pause(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -605,8 +616,9 @@ def async_api_stop(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -620,9 +632,9 @@ def async_api_next(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -636,8 +648,8 @@ def async_api_previous(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.const import (
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
@@ -293,10 +294,11 @@ class APIServicesView(HomeAssistantView):
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
|
||||
@ha.callback
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Get registered services."""
|
||||
return self.json(async_services_json(request.app['hass']))
|
||||
services = yield from async_services_json(request.app['hass'])
|
||||
return self.json(services)
|
||||
|
||||
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
@@ -355,10 +357,12 @@ class APITemplateView(HomeAssistantView):
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
descriptions = yield from async_get_all_descriptions(hass)
|
||||
return [{"domain": key, "services": value}
|
||||
for key, value in hass.services.async_services().items()]
|
||||
for key, value in descriptions.items()]
|
||||
|
||||
|
||||
def async_events_json(hass):
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
|
||||
@@ -12,13 +11,12 @@ import voluptuous as vol
|
||||
|
||||
from typing import Union, TypeVar, Sequence
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.8']
|
||||
REQUIREMENTS = ['pyatv==0.3.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -183,18 +181,12 @@ def async_setup(hass, config):
|
||||
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
|
||||
|
||||
@@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.1.0']
|
||||
REQUIREMENTS = ['pyarlo==0.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -7,14 +7,12 @@ https://home-assistant.io/components/automation/
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import CoreState
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
|
||||
@@ -166,11 +164,6 @@ def async_setup(hass, config):
|
||||
|
||||
yield from _async_process_config(hass, config, component)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def trigger_service_handler(service_call):
|
||||
"""Handle automation triggers."""
|
||||
@@ -216,20 +209,20 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA)
|
||||
schema=TRIGGER_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA)
|
||||
schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||
descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA)
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, turn_onoff_service_handler,
|
||||
descriptions.get(service), schema=SERVICE_SCHEMA)
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ def async_trigger(hass, config, action):
|
||||
|
||||
# Ignore changes to state attributes if from/to is in use
|
||||
if (not match_all and from_s is not None and to_s is not None and
|
||||
from_s.last_changed == to_s.last_changed):
|
||||
from_s.state == to_s.state):
|
||||
return
|
||||
|
||||
if not time_delta:
|
||||
|
||||
@@ -6,12 +6,10 @@ https://home-assistant.io/components/axis/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT,
|
||||
@@ -195,10 +193,6 @@ def setup(hass, config):
|
||||
if not setup_device(hass, config, device_config):
|
||||
_LOGGER.error("Couldn\'t set up %s", device_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():
|
||||
@@ -216,7 +210,6 @@ def setup(hass, config):
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_VAPIX_CALL,
|
||||
vapix_service,
|
||||
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
|
||||
schema=SERVICE_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
@@ -21,23 +21,27 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
DEVICE_CLASSES = [
|
||||
'battery', # On means low, Off means normal
|
||||
'cold', # On means cold (or too cold)
|
||||
'connectivity', # On means connection present, Off = no connection
|
||||
'gas', # CO, CO2, etc.
|
||||
'heat', # On means hot (or too hot)
|
||||
'light', # Lightness threshold
|
||||
'moisture', # Specifically a wetness sensor
|
||||
'motion', # Motion sensor
|
||||
'moving', # On means moving, Off means stopped
|
||||
'occupancy', # On means occupied, Off means not occupied
|
||||
'opening', # Door, window, etc.
|
||||
'cold', # On means cold, Off means normal
|
||||
'connectivity', # On means connected, Off means disconnected
|
||||
'door', # On means open, Off means closed
|
||||
'garage_door', # On means open, Off means closed
|
||||
'gas', # On means gas detected, Off means no gas (clear)
|
||||
'heat', # On means hot, Off means normal
|
||||
'light', # On means light detected, Off means no light
|
||||
'moisture', # On means wet, Off means dry
|
||||
'motion', # On means motion detected, Off means no motion (clear)
|
||||
'moving', # On means moving, Off means not moving (stopped)
|
||||
'occupancy', # On means occupied, Off means not occupied (clear)
|
||||
'opening', # On means open, Off means closed
|
||||
'plug', # On means plugged in, Off means unplugged
|
||||
'power', # Power, over-current, etc
|
||||
'power', # On means power detected, Off means no power
|
||||
'presence', # On means home, Off means away
|
||||
'safety', # Generic on=unsafe, off=safe
|
||||
'smoke', # Smoke detector
|
||||
'sound', # On means sound detected, Off means no sound
|
||||
'problem', # On means problem detected, Off means no problem (OK)
|
||||
'safety', # On means unsafe, Off means safe
|
||||
'smoke', # On means smoke detected, Off means no smoke (clear)
|
||||
'sound', # On means sound detected, Off means no sound (clear)
|
||||
'vibration', # On means vibration detected, Off means no vibration
|
||||
'window', # On means open, Off means closed
|
||||
]
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Support for ADS binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation.
|
||||
https://home-assistant.io/components/binary_sensor.ads/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, \
|
||||
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA
|
||||
from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR
|
||||
from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['ads']
|
||||
DEFAULT_NAME = 'ADS binary sensor'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Binary Sensor platform for ADS."""
|
||||
ads_hub = hass.data.get(DATA_ADS)
|
||||
|
||||
ads_var = config.get(CONF_ADS_VAR)
|
||||
name = config.get(CONF_NAME)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
|
||||
add_devices([ads_sensor])
|
||||
|
||||
|
||||
class AdsBinarySensor(BinarySensorDevice):
|
||||
"""Representation of ADS binary sensors."""
|
||||
|
||||
def __init__(self, ads_hub, name, ads_var, device_class):
|
||||
"""Initialize AdsBinarySensor entity."""
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._device_class = device_class or 'moving'
|
||||
self._ads_hub = ads_hub
|
||||
self.ads_var = ads_var
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
def update(name, value):
|
||||
"""Handle device notifications."""
|
||||
_LOGGER.debug('Variable %s changed its value to %d',
|
||||
name, value)
|
||||
self._state = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.hass.async_add_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the default name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False because entity pushes its state to HA."""
|
||||
return False
|
||||
@@ -7,39 +7,41 @@ https://home-assistant.io/components/binary_sensor.alarmdecoder/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
from homeassistant.components.alarmdecoder import (ZONE_SCHEMA,
|
||||
CONF_ZONES,
|
||||
CONF_ZONE_NAME,
|
||||
CONF_ZONE_TYPE,
|
||||
SIGNAL_ZONE_FAULT,
|
||||
SIGNAL_ZONE_RESTORE)
|
||||
|
||||
from homeassistant.components.alarmdecoder import (
|
||||
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
||||
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
||||
SIGNAL_RFX_MESSAGE)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_RF_BIT0 = 'rf_bit0'
|
||||
ATTR_RF_LOW_BAT = 'rf_low_battery'
|
||||
ATTR_RF_SUPERVISED = 'rf_supervised'
|
||||
ATTR_RF_BIT3 = 'rf_bit3'
|
||||
ATTR_RF_LOOP3 = 'rf_loop3'
|
||||
ATTR_RF_LOOP2 = 'rf_loop2'
|
||||
ATTR_RF_LOOP4 = 'rf_loop4'
|
||||
ATTR_RF_LOOP1 = 'rf_loop1'
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the AlarmDecoder binary sensor devices."""
|
||||
configured_zones = discovery_info[CONF_ZONES]
|
||||
|
||||
devices = []
|
||||
|
||||
for zone_num in configured_zones:
|
||||
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
zone_rfid = device_config_data.get(CONF_ZONE_RFID)
|
||||
device = AlarmDecoderBinarySensor(
|
||||
hass, zone_num, zone_name, zone_type)
|
||||
zone_num, zone_name, zone_type, zone_rfid)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
add_devices(devices)
|
||||
|
||||
return True
|
||||
|
||||
@@ -47,46 +49,52 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an AlarmDecoder binary sensor."""
|
||||
|
||||
def __init__(self, hass, zone_number, zone_name, zone_type):
|
||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._zone_type = zone_type
|
||||
self._state = 0
|
||||
self._state = None
|
||||
self._name = zone_name
|
||||
self._type = zone_type
|
||||
|
||||
_LOGGER.debug("Setup up zone: %s", self._name)
|
||||
self._rfid = zone_rfid
|
||||
self._rfstate = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback)
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_ZONE_RESTORE, self._restore_callback)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon for device by its type."""
|
||||
if "window" in self._name.lower():
|
||||
return "mdi:window-open" if self.is_on else "mdi:window-closed"
|
||||
|
||||
if self._type == 'smoke':
|
||||
return "mdi:fire"
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
if self._rfid and self._rfstate is not None:
|
||||
attr[ATTR_RF_BIT0] = True if self._rfstate & 0x01 else False
|
||||
attr[ATTR_RF_LOW_BAT] = True if self._rfstate & 0x02 else False
|
||||
attr[ATTR_RF_SUPERVISED] = True if self._rfstate & 0x04 else False
|
||||
attr[ATTR_RF_BIT3] = True if self._rfstate & 0x08 else False
|
||||
attr[ATTR_RF_LOOP3] = True if self._rfstate & 0x10 else False
|
||||
attr[ATTR_RF_LOOP2] = True if self._rfstate & 0x20 else False
|
||||
attr[ATTR_RF_LOOP4] = True if self._rfstate & 0x40 else False
|
||||
attr[ATTR_RF_LOOP1] = True if self._rfstate & 0x80 else False
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
@@ -97,16 +105,20 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@callback
|
||||
def _fault_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 1
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _restore_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 0
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _rfx_message_callback(self, message):
|
||||
"""Update RF state."""
|
||||
if self._rfid and message and message.serial_number == self._rfid:
|
||||
self._rfstate = message.value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
Executable → Regular
+9
-2
@@ -15,7 +15,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
REQUIREMENTS = ['concord232==0.15']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -62,6 +62,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return False
|
||||
|
||||
# The order of zones returned by client.list_zones() can vary.
|
||||
# When the zones are not named, this can result in the same entity
|
||||
# name mapping to different sensors in an unpredictable way. Sort
|
||||
# the zones by zone number to prevent this.
|
||||
|
||||
client.zones.sort(key=lambda zone: zone['number'])
|
||||
|
||||
for zone in client.zones:
|
||||
_LOGGER.info("Loading Zone found: %s", zone['name'])
|
||||
if zone['number'] not in exclude:
|
||||
@@ -118,7 +125,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
# True means "faulted" or "open" or "abnormal state"
|
||||
return bool(self._zone['state'] == 'Normal')
|
||||
return bool(self._zone['state'] != 'Normal')
|
||||
|
||||
def update(self):
|
||||
"""Get updated stats from API."""
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Support for deCONZ binary sensor.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
|
||||
DEPENDENCIES = ['deconz']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup binary sensor for deCONZ component."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
sensors = hass.data[DECONZ_DATA].sensors
|
||||
entities = []
|
||||
|
||||
for sensor in sensors.values():
|
||||
if sensor.type in DECONZ_BINARY_SENSOR:
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
|
||||
|
||||
class DeconzBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a binary sensor."""
|
||||
|
||||
def __init__(self, sensor):
|
||||
"""Setup sensor and add update callback to get data from websocket."""
|
||||
self._sensor = sensor
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self._sensor.register_async_callback(self.async_update_callback)
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
"""Update the sensor's state.
|
||||
|
||||
If reason is that state is updated,
|
||||
or reachable has changed or battery has changed.
|
||||
"""
|
||||
if reason['state'] or \
|
||||
'reachable' in reason['attr'] or \
|
||||
'battery' in reason['attr']:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._sensor.is_tripped
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._sensor.name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Class of the sensor."""
|
||||
return self._sensor.sensor_class
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._sensor.sensor_icon
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if sensor is available."""
|
||||
return self._sensor.reachable
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
from pydeconz.sensor import PRESENCE
|
||||
attr = {
|
||||
ATTR_BATTERY_LEVEL: self._sensor.battery,
|
||||
}
|
||||
if self._sensor.type == PRESENCE:
|
||||
attr['dark'] = self._sensor.dark
|
||||
return attr
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Support for reading binary states from a DoorBird video doorbell."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DEPENDENCIES = ['doorbird']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_MIN_UPDATE_INTERVAL = timedelta(milliseconds=250)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
"doorbell": {
|
||||
"name": "Doorbell Ringing",
|
||||
"icon": {
|
||||
True: "bell-ring",
|
||||
False: "bell",
|
||||
None: "bell-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the DoorBird binary sensor component."""
|
||||
device = hass.data.get(DOORBIRD_DOMAIN)
|
||||
add_devices([DoorBirdBinarySensor(device, "doorbell")], True)
|
||||
|
||||
|
||||
class DoorBirdBinarySensor(BinarySensorDevice):
|
||||
"""A binary sensor of a DoorBird device."""
|
||||
|
||||
def __init__(self, device, sensor_type):
|
||||
"""Initialize a binary sensor on a DoorBird device."""
|
||||
self._device = device
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Get the name of the sensor."""
|
||||
return SENSOR_TYPES[self._sensor_type]["name"]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Get an icon to display."""
|
||||
state_icon = SENSOR_TYPES[self._sensor_type]["icon"][self._state]
|
||||
return "mdi:{}".format(state_icon)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Get the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
@Throttle(_MIN_UPDATE_INTERVAL)
|
||||
def update(self):
|
||||
"""Pull the latest value from the device."""
|
||||
self._state = self._device.doorbell_state()
|
||||
@@ -4,67 +4,364 @@ Support for ISY994 binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.isy994/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Callable # noqa
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
ISYDevice)
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
False: STATE_OFF,
|
||||
True: STATE_ON,
|
||||
ISY_DEVICE_TYPES = {
|
||||
'moisture': ['16.8', '16.13', '16.14'],
|
||||
'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'],
|
||||
'motion': ['16.1', '16.4', '16.5', '16.3']
|
||||
}
|
||||
|
||||
UOM = ['2', '78']
|
||||
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 binary sensor platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
devices_by_nid = {}
|
||||
child_nodes = []
|
||||
|
||||
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
|
||||
states=STATES):
|
||||
devices.append(ISYBinarySensorDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
if node.parent_node is None:
|
||||
device = ISYBinarySensorDevice(node)
|
||||
devices.append(device)
|
||||
devices_by_nid[node.nid] = device
|
||||
else:
|
||||
devices.append(ISYBinarySensorProgram(program.name, status))
|
||||
# We'll process the child nodes last, to ensure all parent nodes
|
||||
# have been processed
|
||||
child_nodes.append(node)
|
||||
|
||||
for node in child_nodes:
|
||||
try:
|
||||
parent_device = devices_by_nid[node.parent_node.nid]
|
||||
except KeyError:
|
||||
_LOGGER.error("Node %s has a parent node %s, but no device "
|
||||
"was created for the parent. Skipping.",
|
||||
node.nid, node.parent_nid)
|
||||
else:
|
||||
device_type = _detect_device_type(node)
|
||||
subnode_id = int(node.nid[-1])
|
||||
if device_type == 'opening':
|
||||
# Door/window sensors use an optional "negative" node
|
||||
if subnode_id == 4:
|
||||
# Subnode 4 is the heartbeat node, which we will represent
|
||||
# as a separate binary_sensor
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
elif subnode_id == 2:
|
||||
parent_device.add_negative_node(node)
|
||||
elif device_type == 'moisture':
|
||||
# Moisure nodes have a subnode 2, but we ignore it because it's
|
||||
# just the inverse of the primary node.
|
||||
if subnode_id == 4:
|
||||
# Heartbeat node
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
else:
|
||||
# We don't yet have any special logic for other sensor types,
|
||||
# so add the nodes as individual devices
|
||||
device = ISYBinarySensorDevice(node)
|
||||
devices.append(device)
|
||||
|
||||
for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]:
|
||||
devices.append(ISYBinarySensorProgram(name, status))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor device."""
|
||||
def _detect_device_type(node) -> str:
|
||||
try:
|
||||
device_type = node.type
|
||||
except AttributeError:
|
||||
# The type attribute didn't exist in the ISY's API response
|
||||
return None
|
||||
|
||||
split_type = device_type.split('.')
|
||||
for device_class, ids in ISY_DEVICE_TYPES.items():
|
||||
if '{}.{}'.format(split_type[0], split_type[1]) in ids:
|
||||
return device_class
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _is_val_unknown(val):
|
||||
"""Determine if a number value represents UNKNOWN from PyISY."""
|
||||
return val == -1*float('inf')
|
||||
|
||||
|
||||
class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor device.
|
||||
|
||||
Often times, a single device is represented by multiple nodes in the ISY,
|
||||
allowing for different nuances in how those devices report their on and
|
||||
off events. This class turns those multiple nodes in to a single Hass
|
||||
entity and handles both ways that ISY binary sensors can work.
|
||||
"""
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
self._negative_node = None
|
||||
self._heartbeat_device = None
|
||||
self._device_class_from_type = _detect_device_type(self._node)
|
||||
# pylint: disable=protected-access
|
||||
if _is_val_unknown(self._node.status._val):
|
||||
self._computed_state = None
|
||||
else:
|
||||
self._computed_state = bool(self._node.status._val)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node and subnode event emitters."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
self._node.controlEvents.subscribe(self._positive_node_control_handler)
|
||||
|
||||
if self._negative_node is not None:
|
||||
self._negative_node.controlEvents.subscribe(
|
||||
self._negative_node_control_handler)
|
||||
|
||||
def add_heartbeat_device(self, device) -> None:
|
||||
"""Register a heartbeat device for this sensor.
|
||||
|
||||
The heartbeat node beats on its own, but we can gain a little
|
||||
reliability by considering any node activity for this sensor
|
||||
to be a heartbeat as well.
|
||||
"""
|
||||
self._heartbeat_device = device
|
||||
|
||||
def _heartbeat(self) -> None:
|
||||
"""Send a heartbeat to our heartbeat device, if we have one."""
|
||||
if self._heartbeat_device is not None:
|
||||
self._heartbeat_device.heartbeat()
|
||||
|
||||
def add_negative_node(self, child) -> None:
|
||||
"""Add a negative node to this binary sensor device.
|
||||
|
||||
The negative node is a node that can receive the 'off' events
|
||||
for the sensor, depending on device configuration and type.
|
||||
"""
|
||||
self._negative_node = child
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if not _is_val_unknown(self._negative_node.status._val):
|
||||
# If the negative node has a value, it means the negative node is
|
||||
# in use for this device. Therefore, we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
|
||||
def _negative_node_control_handler(self, event: object) -> None:
|
||||
"""Handle an "On" control event from the "negative" node."""
|
||||
if event == 'DON':
|
||||
_LOGGER.debug("Sensor %s turning Off via the Negative node "
|
||||
"sending a DON command", self.name)
|
||||
self._computed_state = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
def _positive_node_control_handler(self, event: object) -> None:
|
||||
"""Handle On and Off control event coming from the primary node.
|
||||
|
||||
Depending on device configuration, sometimes only On events
|
||||
will come to this node, with the negative node representing Off
|
||||
events
|
||||
"""
|
||||
if event == 'DON':
|
||||
_LOGGER.debug("Sensor %s turning On via the Primary node "
|
||||
"sending a DON command", self.name)
|
||||
self._computed_state = True
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
if event == 'DOF':
|
||||
_LOGGER.debug("Sensor %s turning Off via the Primary node "
|
||||
"sending a DOF command", self.name)
|
||||
self._computed_state = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore primary node status updates.
|
||||
|
||||
We listen directly to the Control events on all nodes for this
|
||||
device.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
"""Get the current value of the device.
|
||||
|
||||
Insteon leak sensors set their primary node to On when the state is
|
||||
DRY, not WET, so we invert the binary state if the user indicates
|
||||
that it is a moisture sensor.
|
||||
"""
|
||||
if self._computed_state is None:
|
||||
# Do this first so we don't invert None on moisture sensors
|
||||
return None
|
||||
|
||||
if self.device_class == 'moisture':
|
||||
return not self._computed_state
|
||||
|
||||
return self._computed_state
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 binary sensor device is on.
|
||||
|
||||
Note: This method will return false if the current state is UNKNOWN
|
||||
"""
|
||||
return bool(self.value)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
if self._computed_state is None:
|
||||
return None
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Return the class of this device.
|
||||
|
||||
This was discovered by parsing the device type code during init
|
||||
"""
|
||||
return self._device_class_from_type
|
||||
|
||||
|
||||
class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
|
||||
"""Representation of the battery state of an ISY994 sensor."""
|
||||
|
||||
def __init__(self, node, parent_device) -> None:
|
||||
"""Initialize the ISY994 binary sensor device."""
|
||||
super().__init__(node)
|
||||
self._computed_state = None
|
||||
self._parent_device = parent_device
|
||||
self._heartbeat_timer = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node and subnode event emitters."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
self._node.controlEvents.subscribe(
|
||||
self._heartbeat_node_control_handler)
|
||||
|
||||
# Start the timer on bootup, so we can change from UNKNOWN to ON
|
||||
self._restart_timer()
|
||||
|
||||
def _heartbeat_node_control_handler(self, event: object) -> None:
|
||||
"""Update the heartbeat timestamp when an On event is sent."""
|
||||
if event == 'DON':
|
||||
self.heartbeat()
|
||||
|
||||
def heartbeat(self):
|
||||
"""Mark the device as online, and restart the 25 hour timer.
|
||||
|
||||
This gets called when the heartbeat node beats, but also when the
|
||||
parent sensor sends any events, as we can trust that to mean the device
|
||||
is online. This mitigates the risk of false positives due to a single
|
||||
missed heartbeat event.
|
||||
"""
|
||||
self._computed_state = False
|
||||
self._restart_timer()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _restart_timer(self):
|
||||
"""Restart the 25 hour timer."""
|
||||
try:
|
||||
self._heartbeat_timer()
|
||||
self._heartbeat_timer = None
|
||||
except TypeError:
|
||||
# No heartbeat timer is active
|
||||
pass
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@callback
|
||||
def timer_elapsed(now) -> None:
|
||||
"""Heartbeat missed; set state to indicate dead battery."""
|
||||
self._computed_state = True
|
||||
self._heartbeat_timer = None
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
point_in_time = dt_util.utcnow() + timedelta(hours=25)
|
||||
_LOGGER.debug("Timer starting. Now: %s Then: %s",
|
||||
dt_util.utcnow(), point_in_time)
|
||||
|
||||
self._heartbeat_timer = async_track_point_in_utc_time(
|
||||
self.hass, timer_elapsed, point_in_time)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore node status updates.
|
||||
|
||||
We listen directly to the Control events for this device.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
"""Get the current value of this sensor."""
|
||||
return self._computed_state
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 binary sensor device is on.
|
||||
|
||||
Note: This method will return false if the current state is UNKNOWN
|
||||
"""
|
||||
return bool(self.value)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
if self._computed_state is None:
|
||||
return None
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Get the class of this device."""
|
||||
return 'battery'
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Get the state attributes for the device."""
|
||||
attr = super().device_state_attributes
|
||||
attr['parent_entity_id'] = self._parent_device.entity_id
|
||||
return attr
|
||||
|
||||
|
||||
class ISYBinarySensorProgram(ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor program.
|
||||
|
||||
This does not need all of the subnode logic in the device version of binary
|
||||
sensors.
|
||||
"""
|
||||
|
||||
def __init__(self, name, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor program."""
|
||||
super().__init__(node)
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 binary sensor device is on."""
|
||||
return bool(self.value)
|
||||
|
||||
|
||||
class ISYBinarySensorProgram(ISYBinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor program."""
|
||||
|
||||
def __init__(self, name, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor program."""
|
||||
ISYBinarySensorDevice.__init__(self, node)
|
||||
self._name = name
|
||||
|
||||
@@ -129,6 +129,11 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.hass.data[DATA_KNX].connected
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed within KNX."""
|
||||
|
||||
@@ -17,19 +17,15 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
|
||||
CONF_DEVICE_CLASS)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_QOS, valid_subscribe_topic)
|
||||
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PAYLOAD_AVAILABLE = 'payload_available'
|
||||
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
|
||||
|
||||
DEFAULT_NAME = 'MQTT Binary sensor'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_AVAILABLE = 'online'
|
||||
DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
@@ -38,12 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_AVAILABILITY_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_AVAILABLE): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
|
||||
})
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -70,31 +61,29 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)])
|
||||
|
||||
|
||||
class MqttBinarySensor(BinarySensorDevice):
|
||||
class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, availability_topic, device_class,
|
||||
qos, payload_on, payload_off, payload_available,
|
||||
payload_not_available, value_template):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._state_topic = state_topic
|
||||
self._availability_topic = availability_topic
|
||||
self._available = True if availability_topic is None else False
|
||||
self._device_class = device_class
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._payload_available = payload_available
|
||||
self._payload_not_available = payload_not_available
|
||||
self._qos = qos
|
||||
self._template = value_template
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
"""Subscribe mqtt events."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def state_message_received(topic, payload, qos):
|
||||
"""Handle a new received MQTT state message."""
|
||||
@@ -111,21 +100,6 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, state_message_received, self._qos)
|
||||
|
||||
@callback
|
||||
def availability_message_received(topic, payload, qos):
|
||||
"""Handle a new received MQTT availability message."""
|
||||
if payload == self._payload_available:
|
||||
self._available = True
|
||||
elif payload == self._payload_not_available:
|
||||
self._available = False
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._availability_topic is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._availability_topic,
|
||||
availability_message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
@@ -136,11 +110,6 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the binary sensor is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
||||
@@ -98,6 +98,11 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
"""Return the class of this sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return the availability of this sensor."""
|
||||
return self.rest.data is not None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
||||
@@ -7,30 +7,40 @@ tested. Other types may need some work.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF, CONF_NAME)
|
||||
from homeassistant.components import rfxtrx
|
||||
from homeassistant.helpers import event as evt
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_NAME, ATTR_DATA_BITS, ATTR_OFF_DELAY, ATTR_FIRE_EVENT,
|
||||
CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT,
|
||||
CONF_DATA_BITS, CONF_DEVICES)
|
||||
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,
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICES, default={}): {
|
||||
cv.string: vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_DATA_BITS): cv.positive_int,
|
||||
vol.Optional(CONF_COMMAND_ON): cv.byte,
|
||||
vol.Optional(CONF_COMMAND_OFF): cv.byte
|
||||
})
|
||||
},
|
||||
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@@ -46,17 +56,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
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]))
|
||||
if entity[CONF_DATA_BITS] is not None:
|
||||
_LOGGER.debug("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])
|
||||
_LOGGER.debug("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_FIRE_EVENT],
|
||||
entity[ATTR_OFF_DELAY],
|
||||
entity[ATTR_DATA_BITS],
|
||||
entity[CONF_COMMAND_ON],
|
||||
@@ -82,15 +92,15 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
if sensor is None:
|
||||
# Add the entity if not exists and automatic_add is True
|
||||
if not config[ATTR_AUTOMATIC_ADD]:
|
||||
if not config[CONF_AUTOMATIC_ADD]:
|
||||
return
|
||||
|
||||
if event.device.packettype == 0x13:
|
||||
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
|
||||
if poss_dev is not None:
|
||||
poss_id = slugify(poss_dev.event.device.id_string.lower())
|
||||
_LOGGER.info("Found possible matching deviceid %s.",
|
||||
poss_id)
|
||||
_LOGGER.debug("Found possible matching deviceid %s.",
|
||||
poss_id)
|
||||
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
sensor = RfxtrxBinarySensor(event, pkt_id)
|
||||
@@ -107,11 +117,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
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)
|
||||
_LOGGER.debug("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_lighting4:
|
||||
if sensor.data_bits is not None:
|
||||
@@ -163,10 +173,8 @@ class RfxtrxBinarySensor(BinarySensorDevice):
|
||||
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
|
||||
else:
|
||||
self._masked_id = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -38,6 +38,11 @@ SENSOR_SCHEMA = vol.Schema({
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
})
|
||||
|
||||
SENSOR_SCHEMA = vol.All(
|
||||
cv.deprecated(ATTR_ENTITY_ID),
|
||||
SENSOR_SCHEMA,
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
|
||||
})
|
||||
|
||||
@@ -9,40 +9,48 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN,
|
||||
ATTR_ENTITY_ID, CONF_DEVICE_CLASS)
|
||||
ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_HYSTERESIS = 'hysteresis'
|
||||
ATTR_LOWER = 'lower'
|
||||
ATTR_POSITION = 'position'
|
||||
ATTR_SENSOR_VALUE = 'sensor_value'
|
||||
ATTR_THRESHOLD = 'threshold'
|
||||
ATTR_TYPE = 'type'
|
||||
ATTR_UPPER = 'upper'
|
||||
|
||||
CONF_HYSTERESIS = 'hysteresis'
|
||||
CONF_LOWER = 'lower'
|
||||
CONF_THRESHOLD = 'threshold'
|
||||
CONF_UPPER = 'upper'
|
||||
|
||||
DEFAULT_NAME = 'Threshold'
|
||||
DEFAULT_HYSTERESIS = 0.0
|
||||
|
||||
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER]
|
||||
POSITION_ABOVE = 'above'
|
||||
POSITION_BELOW = 'below'
|
||||
POSITION_IN_RANGE = 'in_range'
|
||||
POSITION_UNKNOWN = 'unknown'
|
||||
|
||||
TYPE_LOWER = 'lower'
|
||||
TYPE_RANGE = 'range'
|
||||
TYPE_UPPER = 'upper'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
|
||||
vol.Optional(
|
||||
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS):
|
||||
vol.Coerce(float),
|
||||
vol.Optional(CONF_LOWER): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_UPPER): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
@@ -51,47 +59,44 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Threshold sensor."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
name = config.get(CONF_NAME)
|
||||
threshold = config.get(CONF_THRESHOLD)
|
||||
lower = config.get(CONF_LOWER)
|
||||
upper = config.get(CONF_UPPER)
|
||||
hysteresis = config.get(CONF_HYSTERESIS)
|
||||
limit_type = config.get(CONF_TYPE)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_devices([ThresholdSensor(
|
||||
hass, entity_id, name, threshold,
|
||||
hysteresis, limit_type, device_class)
|
||||
], True)
|
||||
|
||||
return True
|
||||
hass, entity_id, name, lower, upper, hysteresis, device_class)], True)
|
||||
|
||||
|
||||
class ThresholdSensor(BinarySensorDevice):
|
||||
"""Representation of a Threshold sensor."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, threshold,
|
||||
hysteresis, limit_type, device_class):
|
||||
def __init__(self, hass, entity_id, name, lower, upper, hysteresis,
|
||||
device_class):
|
||||
"""Initialize the Threshold sensor."""
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self.is_upper = limit_type == 'upper'
|
||||
self._name = name
|
||||
self._threshold = threshold
|
||||
self._threshold_lower = lower
|
||||
self._threshold_upper = upper
|
||||
self._hysteresis = hysteresis
|
||||
self._device_class = device_class
|
||||
self._state = False
|
||||
self.sensor_value = 0
|
||||
|
||||
@callback
|
||||
self._state_position = None
|
||||
self._state = False
|
||||
self.sensor_value = None
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@callback
|
||||
def async_threshold_sensor_state_listener(
|
||||
entity, old_state, new_state):
|
||||
"""Handle sensor state changes."""
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
return
|
||||
|
||||
try:
|
||||
self.sensor_value = float(new_state.state)
|
||||
except ValueError:
|
||||
_LOGGER.error("State is not numerical")
|
||||
self.sensor_value = None if new_state.state == STATE_UNKNOWN \
|
||||
else float(new_state.state)
|
||||
except (ValueError, TypeError):
|
||||
self.sensor_value = None
|
||||
_LOGGER.warning("State is not numerical")
|
||||
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
@@ -118,23 +123,67 @@ class ThresholdSensor(BinarySensorDevice):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def threshold_type(self):
|
||||
"""Return the type of threshold this sensor represents."""
|
||||
if self._threshold_lower and self._threshold_upper:
|
||||
return TYPE_RANGE
|
||||
elif self._threshold_lower:
|
||||
return TYPE_LOWER
|
||||
elif self._threshold_upper:
|
||||
return TYPE_UPPER
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_SENSOR_VALUE: self.sensor_value,
|
||||
ATTR_THRESHOLD: self._threshold,
|
||||
ATTR_HYSTERESIS: self._hysteresis,
|
||||
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
|
||||
ATTR_LOWER: self._threshold_lower,
|
||||
ATTR_POSITION: self._state_position,
|
||||
ATTR_SENSOR_VALUE: self.sensor_value,
|
||||
ATTR_TYPE: self.threshold_type,
|
||||
ATTR_UPPER: self._threshold_upper,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
if self._hysteresis == 0 and self.sensor_value == self._threshold:
|
||||
def below(threshold):
|
||||
"""Determine if the sensor value is below a threshold."""
|
||||
return self.sensor_value < (threshold - self._hysteresis)
|
||||
|
||||
def above(threshold):
|
||||
"""Determine if the sensor value is above a threshold."""
|
||||
return self.sensor_value > (threshold + self._hysteresis)
|
||||
|
||||
if self.sensor_value is None:
|
||||
self._state_position = POSITION_UNKNOWN
|
||||
self._state = False
|
||||
elif self.sensor_value > (self._threshold + self._hysteresis):
|
||||
self._state = self.is_upper
|
||||
elif self.sensor_value < (self._threshold - self._hysteresis):
|
||||
self._state = not self.is_upper
|
||||
|
||||
elif self.threshold_type == TYPE_LOWER:
|
||||
if below(self._threshold_lower):
|
||||
self._state_position = POSITION_BELOW
|
||||
self._state = True
|
||||
elif above(self._threshold_lower):
|
||||
self._state_position = POSITION_ABOVE
|
||||
self._state = False
|
||||
|
||||
elif self.threshold_type == TYPE_UPPER:
|
||||
if above(self._threshold_upper):
|
||||
self._state_position = POSITION_ABOVE
|
||||
self._state = True
|
||||
elif below(self._threshold_upper):
|
||||
self._state_position = POSITION_BELOW
|
||||
self._state = False
|
||||
|
||||
elif self.threshold_type == TYPE_RANGE:
|
||||
if below(self._threshold_lower):
|
||||
self._state_position = POSITION_BELOW
|
||||
self._state = False
|
||||
if above(self._threshold_upper):
|
||||
self._state_position = POSITION_ABOVE
|
||||
self._state = False
|
||||
elif above(self._threshold_lower) and below(self._threshold_upper):
|
||||
self._state_position = POSITION_IN_RANGE
|
||||
self._state = True
|
||||
|
||||
@@ -11,21 +11,19 @@ import math
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||
BinarySensorDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_DEVICE_CLASS, CONF_ENTITY_ID,
|
||||
CONF_FRIENDLY_NAME, STATE_UNKNOWN)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME,
|
||||
CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.13.3']
|
||||
REQUIREMENTS = ['numpy==1.14.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,21 +34,21 @@ ATTR_INVERT = 'invert'
|
||||
ATTR_SAMPLE_DURATION = 'sample_duration'
|
||||
ATTR_SAMPLE_COUNT = 'sample_count'
|
||||
|
||||
CONF_SENSORS = 'sensors'
|
||||
CONF_ATTRIBUTE = 'attribute'
|
||||
CONF_INVERT = 'invert'
|
||||
CONF_MAX_SAMPLES = 'max_samples'
|
||||
CONF_MIN_GRADIENT = 'min_gradient'
|
||||
CONF_INVERT = 'invert'
|
||||
CONF_SAMPLE_DURATION = 'sample_duration'
|
||||
CONF_SENSORS = 'sensors'
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_MAX_SAMPLES, default=2): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_GRADIENT, default=0.0): vol.Coerce(float),
|
||||
vol.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SAMPLE_DURATION, default=0): cv.positive_int,
|
||||
})
|
||||
|
||||
@@ -129,11 +127,11 @@ class SensorTrend(BinarySensorDevice):
|
||||
return {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_FRIENDLY_NAME: self._name,
|
||||
ATTR_INVERT: self._invert,
|
||||
ATTR_GRADIENT: self._gradient,
|
||||
ATTR_INVERT: self._invert,
|
||||
ATTR_MIN_GRADIENT: self._min_gradient,
|
||||
ATTR_SAMPLE_DURATION: self._sample_duration,
|
||||
ATTR_SAMPLE_COUNT: len(self.samples),
|
||||
ATTR_SAMPLE_DURATION: self._sample_duration,
|
||||
}
|
||||
|
||||
@property
|
||||
|
||||
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Vera controller devices."""
|
||||
add_devices(
|
||||
VeraBinarySensor(device, VERA_CONTROLLER)
|
||||
for device in VERA_DEVICES['binary_sensor'])
|
||||
VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
|
||||
for device in hass.data[VERA_DEVICES]['binary_sensor'])
|
||||
|
||||
|
||||
class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
|
||||
@@ -64,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
excludes = config.get(CONF_EXCLUDES)
|
||||
days_offset = config.get(CONF_OFFSET)
|
||||
|
||||
year = (datetime.now() + timedelta(days=days_offset)).year
|
||||
year = (get_date(datetime.today()) + timedelta(days=days_offset)).year
|
||||
obj_holidays = getattr(holidays, country)(years=year)
|
||||
|
||||
if province:
|
||||
@@ -99,6 +99,11 @@ def day_to_string(day):
|
||||
return None
|
||||
|
||||
|
||||
def get_date(date):
|
||||
"""Return date. Needed for testing."""
|
||||
return date
|
||||
|
||||
|
||||
class IsWorkdaySensor(BinarySensorDevice):
|
||||
"""Implementation of a Workday sensor."""
|
||||
|
||||
@@ -156,7 +161,7 @@ class IsWorkdaySensor(BinarySensorDevice):
|
||||
self._state = False
|
||||
|
||||
# Get iso day of the week (1 = Monday, 7 = Sunday)
|
||||
date = datetime.today() + timedelta(days=self._days_offset)
|
||||
date = get_date(datetime.today()) + timedelta(days=self._days_offset)
|
||||
day = date.isoweekday() - 1
|
||||
day_of_week = day_to_string(day)
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
Support for WebDav Calendar.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar.caldav/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.calendar import (
|
||||
CalendarEventDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
|
||||
from homeassistant.util import dt, Throttle
|
||||
|
||||
REQUIREMENTS = ['caldav==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
CONF_CALENDARS = 'calendars'
|
||||
CONF_CUSTOM_CALENDARS = 'custom_calendars'
|
||||
CONF_CALENDAR = 'calendar'
|
||||
CONF_SEARCH = 'search'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Required(CONF_URL): vol.Url(),
|
||||
vol.Optional(CONF_CALENDARS, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Schema([
|
||||
cv.string
|
||||
])),
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_CUSTOM_CALENDARS, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Schema([
|
||||
vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_CALENDAR): cv.string,
|
||||
vol.Required(CONF_SEARCH): cv.string
|
||||
})
|
||||
]))
|
||||
})
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
"""Set up the WebDav Calendar platform."""
|
||||
import caldav
|
||||
|
||||
client = caldav.DAVClient(config.get(CONF_URL),
|
||||
None,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD))
|
||||
|
||||
# Retrieve all the remote calendars
|
||||
calendars = client.principal().calendars()
|
||||
|
||||
calendar_devices = []
|
||||
for calendar in list(calendars):
|
||||
# If a calendar name was given in the configuration,
|
||||
# ignore all the others
|
||||
if (config.get(CONF_CALENDARS)
|
||||
and calendar.name not in config.get(CONF_CALENDARS)):
|
||||
_LOGGER.debug("Ignoring calendar '%s'", calendar.name)
|
||||
continue
|
||||
|
||||
# Create additional calendars based on custom filtering
|
||||
# rules
|
||||
for cust_calendar in config.get(CONF_CUSTOM_CALENDARS):
|
||||
# Check that the base calendar matches
|
||||
if cust_calendar.get(CONF_CALENDAR) != calendar.name:
|
||||
continue
|
||||
|
||||
device_data = {
|
||||
CONF_NAME: cust_calendar.get(CONF_NAME),
|
||||
CONF_DEVICE_ID: "{} {}".format(
|
||||
cust_calendar.get(CONF_CALENDAR),
|
||||
cust_calendar.get(CONF_NAME)),
|
||||
}
|
||||
|
||||
calendar_devices.append(
|
||||
WebDavCalendarEventDevice(hass,
|
||||
device_data,
|
||||
calendar,
|
||||
True,
|
||||
cust_calendar.get(CONF_SEARCH))
|
||||
)
|
||||
|
||||
# Create a default calendar if there was no custom one
|
||||
if not config.get(CONF_CUSTOM_CALENDARS):
|
||||
device_data = {
|
||||
CONF_NAME: calendar.name,
|
||||
CONF_DEVICE_ID: calendar.name
|
||||
}
|
||||
calendar_devices.append(
|
||||
WebDavCalendarEventDevice(hass, device_data, calendar)
|
||||
)
|
||||
|
||||
# Finally add all the calendars we've created
|
||||
add_devices(calendar_devices)
|
||||
|
||||
|
||||
class WebDavCalendarEventDevice(CalendarEventDevice):
|
||||
"""A device for getting the next Task from a WebDav Calendar."""
|
||||
|
||||
def __init__(self,
|
||||
hass,
|
||||
device_data,
|
||||
calendar,
|
||||
all_day=False,
|
||||
search=None):
|
||||
"""Create the WebDav Calendar Event Device."""
|
||||
self.data = WebDavCalendarData(calendar, all_day, search)
|
||||
super().__init__(hass, device_data)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
if self.data.event is None:
|
||||
# No tasks, we don't REALLY need to show anything.
|
||||
return {}
|
||||
|
||||
attributes = super().device_state_attributes
|
||||
return attributes
|
||||
|
||||
|
||||
class WebDavCalendarData(object):
|
||||
"""Class to utilize the calendar dav client object to get next event."""
|
||||
|
||||
def __init__(self, calendar, include_all_day, search):
|
||||
"""Set up how we are going to search the WebDav calendar."""
|
||||
self.calendar = calendar
|
||||
self.include_all_day = include_all_day
|
||||
self.search = search
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
# We have to retrieve the results for the whole day as the server
|
||||
# won't return events that have already started
|
||||
results = self.calendar.date_search(
|
||||
dt.start_of_local_day(),
|
||||
dt.start_of_local_day() + timedelta(days=1)
|
||||
)
|
||||
|
||||
# dtstart can be a date or datetime depending if the event lasts a
|
||||
# whole day. Convert everything to datetime to be able to sort it
|
||||
results.sort(key=lambda x: self.to_datetime(
|
||||
x.instance.vevent.dtstart.value
|
||||
))
|
||||
|
||||
vevent = next((
|
||||
event.instance.vevent for event in results
|
||||
if (self.is_matching(event.instance.vevent, self.search)
|
||||
and (not self.is_all_day(event.instance.vevent)
|
||||
or self.include_all_day)
|
||||
and not self.is_over(event.instance.vevent))), None)
|
||||
|
||||
# If no matching event could be found
|
||||
if vevent is None:
|
||||
_LOGGER.debug(
|
||||
"No matching event found in the %d results for %s",
|
||||
len(results),
|
||||
self.calendar.name,
|
||||
)
|
||||
self.event = None
|
||||
return True
|
||||
|
||||
# Populate the entity attributes with the event values
|
||||
self.event = {
|
||||
"summary": vevent.summary.value,
|
||||
"start": self.get_hass_date(vevent.dtstart.value),
|
||||
"end": self.get_hass_date(vevent.dtend.value),
|
||||
"location": self.get_attr_value(vevent, "location"),
|
||||
"description": self.get_attr_value(vevent, "description")
|
||||
}
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_matching(vevent, search):
|
||||
"""Return if the event matches the filter critera."""
|
||||
if search is None:
|
||||
return True
|
||||
|
||||
pattern = re.compile(search)
|
||||
return (hasattr(vevent, "summary")
|
||||
and pattern.match(vevent.summary.value)
|
||||
or hasattr(vevent, "location")
|
||||
and pattern.match(vevent.location.value)
|
||||
or hasattr(vevent, "description")
|
||||
and pattern.match(vevent.description.value))
|
||||
|
||||
@staticmethod
|
||||
def is_all_day(vevent):
|
||||
"""Return if the event last the whole day."""
|
||||
return not isinstance(vevent.dtstart.value, datetime)
|
||||
|
||||
@staticmethod
|
||||
def is_over(vevent):
|
||||
"""Return if the event is over."""
|
||||
return dt.now() > WebDavCalendarData.to_datetime(vevent.dtend.value)
|
||||
|
||||
@staticmethod
|
||||
def get_hass_date(obj):
|
||||
"""Return if the event matches."""
|
||||
if isinstance(obj, datetime):
|
||||
return {"dateTime": obj.isoformat()}
|
||||
|
||||
return {"date": obj.isoformat()}
|
||||
|
||||
@staticmethod
|
||||
def to_datetime(obj):
|
||||
"""Return a datetime."""
|
||||
if isinstance(obj, datetime):
|
||||
return obj
|
||||
return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min))
|
||||
|
||||
@staticmethod
|
||||
def get_attr_value(obj, attribute):
|
||||
"""Return the value of the attribute if defined."""
|
||||
if hasattr(obj, attribute):
|
||||
return getattr(obj, attribute).value
|
||||
return None
|
||||
Executable → Regular
@@ -9,7 +9,6 @@ https://home-assistant.io/components/calendar.todoist/
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -17,7 +16,6 @@ from homeassistant.components.calendar import (
|
||||
CalendarEventDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.google import (
|
||||
CONF_DEVICE_ID)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
CONF_ID, CONF_NAME, CONF_TOKEN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -178,10 +176,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices(project_devices)
|
||||
|
||||
# Services:
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def handle_new_task(call):
|
||||
"""Called when a user creates a new Todoist Task from HASS."""
|
||||
project_name = call.data[PROJECT_NAME]
|
||||
@@ -215,7 +209,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.debug("Created Todoist task: %s", call.data[CONTENT])
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_NEW_TASK, handle_new_task,
|
||||
descriptions[DOMAIN][SERVICE_NEW_TASK],
|
||||
schema=NEW_TASK_SERVICE_SCHEMA)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
from random import SystemRandom
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
@@ -21,7 +20,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
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.loader import bind_hass
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -190,19 +188,14 @@ def async_setup(hass, config):
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write image to file: %s", err)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_ENABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DISABLE_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_DISABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SNAPSHOT, async_handle_snapshot_service,
|
||||
descriptions.get(SERVICE_SNAPSHOT),
|
||||
schema=CAMERA_SERVICE_SNAPSHOT)
|
||||
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Support for Canary camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.canary/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT
|
||||
|
||||
DEPENDENCIES = ['canary']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MOTION_START_TIME = "motion_start_time"
|
||||
ATTR_MOTION_END_TIME = "motion_end_time"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Canary sensors."""
|
||||
data = hass.data[DATA_CANARY]
|
||||
devices = []
|
||||
|
||||
for location in data.locations:
|
||||
entries = data.get_motion_entries(location.location_id)
|
||||
if entries:
|
||||
devices.append(CanaryCamera(data, location.location_id,
|
||||
DEFAULT_TIMEOUT))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class CanaryCamera(Camera):
|
||||
"""An implementation of a Canary security camera."""
|
||||
|
||||
def __init__(self, data, location_id, timeout):
|
||||
"""Initialize a Canary security camera."""
|
||||
super().__init__()
|
||||
self._data = data
|
||||
self._location_id = location_id
|
||||
self._timeout = timeout
|
||||
|
||||
self._location = None
|
||||
self._motion_entry = None
|
||||
self._image_content = None
|
||||
|
||||
def camera_image(self):
|
||||
"""Update the status of the camera and return bytes of camera image."""
|
||||
self.update()
|
||||
return self._image_content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._location.name
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return self._location.is_recording
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
if self._motion_entry is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_MOTION_START_TIME: self._motion_entry.start_time,
|
||||
ATTR_MOTION_END_TIME: self._motion_entry.end_time,
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Update the status of the camera."""
|
||||
self._data.update()
|
||||
self._location = self._data.get_location(self._location_id)
|
||||
|
||||
entries = self._data.get_motion_entries(self._location_id)
|
||||
if entries:
|
||||
current = entries[0]
|
||||
previous = self._motion_entry
|
||||
|
||||
if previous is None or previous.entry_id != current.entry_id:
|
||||
self._motion_entry = current
|
||||
self._image_content = requests.get(
|
||||
current.thumbnails[0].image_url,
|
||||
timeout=self._timeout).content
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return not self._location.is_recording
|
||||
@@ -1,51 +1,40 @@
|
||||
"""Support for viewing the camera feed from a DoorBird video doorbell."""
|
||||
"""
|
||||
Support for viewing the camera feed from a DoorBird video doorbell.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.doorbird/
|
||||
"""
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
DEPENDENCIES = ['doorbird']
|
||||
|
||||
_CAMERA_LIVE = "DoorBird Live"
|
||||
_CAMERA_LAST_VISITOR = "DoorBird Last Ring"
|
||||
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
|
||||
_CAMERA_LIVE = "DoorBird Live"
|
||||
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
|
||||
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_TIMEOUT = 10 # seconds
|
||||
|
||||
CONF_SHOW_LAST_VISITOR = 'last_visitor'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SHOW_LAST_VISITOR, default=False): cv.boolean
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the DoorBird camera platform."""
|
||||
device = hass.data.get(DOORBIRD_DOMAIN)
|
||||
|
||||
_LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LIVE)
|
||||
entities = [DoorBirdCamera(device.live_image_url, _CAMERA_LIVE,
|
||||
_LIVE_INTERVAL)]
|
||||
|
||||
if config.get(CONF_SHOW_LAST_VISITOR):
|
||||
_LOGGER.debug("Adding DoorBird camera %s", _CAMERA_LAST_VISITOR)
|
||||
entities.append(DoorBirdCamera(device.history_image_url(1),
|
||||
_CAMERA_LAST_VISITOR,
|
||||
_LAST_VISITOR_INTERVAL))
|
||||
|
||||
async_add_devices(entities)
|
||||
_LOGGER.info("Added DoorBird camera(s)")
|
||||
async_add_devices([
|
||||
DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, _LIVE_INTERVAL),
|
||||
DoorBirdCamera(
|
||||
device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR,
|
||||
_LAST_VISITOR_INTERVAL),
|
||||
])
|
||||
|
||||
|
||||
class DoorBirdCamera(Camera):
|
||||
@@ -75,7 +64,6 @@ class DoorBirdCamera(Camera):
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
|
||||
with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop):
|
||||
response = yield from websession.get(self._url)
|
||||
|
||||
|
||||
Executable → Regular
+2
-4
@@ -60,11 +60,9 @@ class MqttCamera(Camera):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe MQTT events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
"""Subscribe MQTT events."""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Handle new MQTT messages."""
|
||||
|
||||
@@ -82,6 +82,7 @@ class UnifiVideoCamera(Camera):
|
||||
self.is_streaming = False
|
||||
self._connect_addr = None
|
||||
self._camera = None
|
||||
self._motion_status = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -94,6 +95,12 @@ class UnifiVideoCamera(Camera):
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
return caminfo['recordingSettings']['fullTimeRecordEnabled']
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Camera Motion Detection Status."""
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
return caminfo['recordingSettings']['motionRecordEnabled']
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the brand of this camera."""
|
||||
@@ -165,3 +172,26 @@ class UnifiVideoCamera(Camera):
|
||||
raise
|
||||
|
||||
return _get_image()
|
||||
|
||||
def set_motion_detection(self, mode):
|
||||
"""Set motion detection on or off."""
|
||||
from uvcclient.nvr import NvrError
|
||||
if mode is True:
|
||||
set_mode = 'motion'
|
||||
else:
|
||||
set_mode = 'none'
|
||||
|
||||
try:
|
||||
self._nvr.set_recordmode(self._uuid, set_mode)
|
||||
self._motion_status = mode
|
||||
except NvrError as err:
|
||||
_LOGGER.error("Unable to set recordmode to " + set_mode)
|
||||
_LOGGER.debug(err)
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in camera."""
|
||||
self.set_motion_detection(True)
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable motion detection in camera."""
|
||||
self.set_motion_detection(False)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Support for Canary.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/canary/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
from requests import ConnectTimeout, HTTPError
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['py-canary==0.2.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NOTIFICATION_ID = 'canary_notification'
|
||||
NOTIFICATION_TITLE = 'Canary Setup'
|
||||
|
||||
DOMAIN = 'canary'
|
||||
DATA_CANARY = 'canary'
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
CANARY_COMPONENTS = [
|
||||
'alarm_control_panel', 'camera', 'sensor'
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Canary component."""
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
timeout = conf.get(CONF_TIMEOUT)
|
||||
|
||||
try:
|
||||
hass.data[DATA_CANARY] = CanaryData(username, password, timeout)
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Canary service: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
for component in CANARY_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CanaryData(object):
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, username, password, timeout):
|
||||
"""Init the Canary data object."""
|
||||
from canary.api import Api
|
||||
self._api = Api(username, password, timeout)
|
||||
|
||||
self._locations_by_id = {}
|
||||
self._readings_by_device_id = {}
|
||||
self._entries_by_location_id = {}
|
||||
|
||||
self.update()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self, **kwargs):
|
||||
"""Get the latest data from py-canary."""
|
||||
for location in self._api.get_locations():
|
||||
location_id = location.location_id
|
||||
|
||||
self._locations_by_id[location_id] = location
|
||||
self._entries_by_location_id[location_id] = self._api.get_entries(
|
||||
location_id, entry_type="motion", limit=1)
|
||||
|
||||
for device in location.devices:
|
||||
if device.is_online:
|
||||
self._readings_by_device_id[device.device_id] = \
|
||||
self._api.get_latest_readings(device.device_id)
|
||||
|
||||
@property
|
||||
def locations(self):
|
||||
"""Return a list of locations."""
|
||||
return self._locations_by_id.values()
|
||||
|
||||
def get_motion_entries(self, location_id):
|
||||
"""Return a list of motion entries based on location_id."""
|
||||
return self._entries_by_location_id.get(location_id, [])
|
||||
|
||||
def get_location(self, location_id):
|
||||
"""Return a location based on location_id."""
|
||||
return self._locations_by_id.get(location_id, [])
|
||||
|
||||
def get_readings(self, device_id):
|
||||
"""Return a list of readings based on device_id."""
|
||||
return self._readings_by_device_id.get(device_id, [])
|
||||
|
||||
def set_location_mode(self, location_id, mode_name, is_private=False):
|
||||
"""Set location mode."""
|
||||
self._api.set_location_mode(location_id, mode_name, is_private)
|
||||
self.update(no_throttle=True)
|
||||
@@ -7,12 +7,10 @@ https://home-assistant.io/components/climate/
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import functools as ft
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.temperature import display_temp as show_temp
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
@@ -21,9 +19,9 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS)
|
||||
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
|
||||
PRECISION_TENTHS, )
|
||||
DOMAIN = 'climate'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@@ -63,6 +61,7 @@ SUPPORT_HOLD_MODE = 256
|
||||
SUPPORT_SWING_MODE = 512
|
||||
SUPPORT_AWAY_MODE = 1024
|
||||
SUPPORT_AUX_HEAT = 2048
|
||||
SUPPORT_ON_OFF = 4096
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
|
||||
ATTR_MAX_TEMP = 'max_temp'
|
||||
@@ -92,6 +91,10 @@ CONVERTIBLE_ATTRIBUTE = [
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ON_OFF_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
SET_AWAY_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_AWAY_MODE): cv.boolean,
|
||||
@@ -240,10 +243,6 @@ def async_setup(hass, config):
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
yield from component.async_setup(config)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_away_mode_set_service(service):
|
||||
"""Set away mode on target climate devices."""
|
||||
@@ -267,7 +266,6 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_AWAY_MODE),
|
||||
schema=SET_AWAY_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -290,7 +288,6 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_HOLD_MODE),
|
||||
schema=SET_HOLD_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -316,7 +313,6 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
|
||||
descriptions.get(SERVICE_SET_AUX_HEAT),
|
||||
schema=SET_AUX_HEAT_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -348,7 +344,6 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
|
||||
descriptions.get(SERVICE_SET_TEMPERATURE),
|
||||
schema=SET_TEMPERATURE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -370,7 +365,6 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
|
||||
descriptions.get(SERVICE_SET_HUMIDITY),
|
||||
schema=SET_HUMIDITY_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -392,7 +386,6 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_FAN_MODE),
|
||||
schema=SET_FAN_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -414,7 +407,6 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
|
||||
descriptions.get(SERVICE_SET_OPERATION_MODE),
|
||||
schema=SET_OPERATION_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -436,9 +428,34 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
|
||||
descriptions.get(SERVICE_SET_SWING_MODE),
|
||||
schema=SET_SWING_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_on_off_service(service):
|
||||
"""Handle on/off calls."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
yield from climate.async_turn_on()
|
||||
elif service.service == SERVICE_TURN_OFF:
|
||||
yield from climate.async_turn_off()
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_on_off_service,
|
||||
schema=ON_OFF_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_ON, async_on_off_service,
|
||||
schema=ON_OFF_SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -449,8 +466,12 @@ class ClimateDevice(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
if self.is_on is False:
|
||||
return STATE_OFF
|
||||
if self.current_operation:
|
||||
return self.current_operation
|
||||
if self.is_on:
|
||||
return STATE_ON
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
@@ -594,6 +615,11 @@ class ClimateDevice(Entity):
|
||||
"""Return the current hold mode, e.g., home, away, temp."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if aux heater."""
|
||||
@@ -730,6 +756,28 @@ class ClimateDevice(Entity):
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_aux_heat_off)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn device on."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_on(self):
|
||||
"""Turn device on.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_on)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn device off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_off(self):
|
||||
"""Turn device off.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_off)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Support for the Daikin HVAC.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.daikin/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, ATTR_FAN_MODE, ATTR_SWING_MODE,
|
||||
ATTR_CURRENT_TEMPERATURE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_SWING_MODE, STATE_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL,
|
||||
STATE_DRY, STATE_FAN_ONLY
|
||||
)
|
||||
from homeassistant.components.daikin import (
|
||||
daikin_api_setup,
|
||||
ATTR_TARGET_TEMPERATURE,
|
||||
ATTR_INSIDE_TEMPERATURE,
|
||||
ATTR_OUTSIDE_TEMPERATURE
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME,
|
||||
TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE
|
||||
)
|
||||
|
||||
REQUIREMENTS = ['pydaikin==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_SWING_MODE)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
})
|
||||
|
||||
HA_STATE_TO_DAIKIN = {
|
||||
STATE_FAN_ONLY: 'fan',
|
||||
STATE_DRY: 'dry',
|
||||
STATE_COOL: 'cool',
|
||||
STATE_HEAT: 'hot',
|
||||
STATE_AUTO: 'auto',
|
||||
STATE_OFF: 'off',
|
||||
}
|
||||
|
||||
HA_ATTR_TO_DAIKIN = {
|
||||
ATTR_OPERATION_MODE: 'mode',
|
||||
ATTR_FAN_MODE: 'f_rate',
|
||||
ATTR_SWING_MODE: 'f_dir',
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Daikin HVAC platform."""
|
||||
if discovery_info is not None:
|
||||
host = discovery_info.get('ip')
|
||||
name = None
|
||||
_LOGGER.info("Discovered a Daikin AC on %s", host)
|
||||
else:
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
_LOGGER.info("Added Daikin AC on %s", host)
|
||||
|
||||
api = daikin_api_setup(hass, host, name)
|
||||
add_devices([DaikinClimate(api)], True)
|
||||
|
||||
|
||||
class DaikinClimate(ClimateDevice):
|
||||
"""Representation of a Daikin HVAC."""
|
||||
|
||||
def __init__(self, api):
|
||||
"""Initialize the climate device."""
|
||||
from pydaikin import appliance
|
||||
|
||||
self._api = api
|
||||
self._force_refresh = False
|
||||
self._list = {
|
||||
ATTR_OPERATION_MODE: list(
|
||||
map(str.title, set(HA_STATE_TO_DAIKIN.values()))
|
||||
),
|
||||
ATTR_FAN_MODE: list(
|
||||
map(
|
||||
str.title,
|
||||
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])
|
||||
)
|
||||
),
|
||||
ATTR_SWING_MODE: list(
|
||||
map(
|
||||
str.title,
|
||||
appliance.daikin_values(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE])
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
def get(self, key):
|
||||
"""Retrieve device settings from API library cache."""
|
||||
value = None
|
||||
cast_to_float = False
|
||||
|
||||
if key in [ATTR_TEMPERATURE, ATTR_INSIDE_TEMPERATURE,
|
||||
ATTR_CURRENT_TEMPERATURE]:
|
||||
value = self._api.device.values.get('htemp')
|
||||
cast_to_float = True
|
||||
if key == ATTR_TARGET_TEMPERATURE:
|
||||
value = self._api.device.values.get('stemp')
|
||||
cast_to_float = True
|
||||
elif key == ATTR_OUTSIDE_TEMPERATURE:
|
||||
value = self._api.device.values.get('otemp')
|
||||
cast_to_float = True
|
||||
elif key == ATTR_FAN_MODE:
|
||||
value = self._api.device.represent('f_rate')[1].title()
|
||||
elif key == ATTR_SWING_MODE:
|
||||
value = self._api.device.represent('f_dir')[1].title()
|
||||
elif key == ATTR_OPERATION_MODE:
|
||||
# Daikin can return also internal states auto-1 or auto-7
|
||||
# and we need to translate them as AUTO
|
||||
value = re.sub(
|
||||
'[^a-z]',
|
||||
'',
|
||||
self._api.device.represent('mode')[1]
|
||||
).title()
|
||||
|
||||
if value is None:
|
||||
_LOGGER.warning("Invalid value requested for key %s", key)
|
||||
else:
|
||||
if value == "-" or value == "--":
|
||||
value = None
|
||||
elif cast_to_float:
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
value = None
|
||||
|
||||
return value
|
||||
|
||||
def set(self, settings):
|
||||
"""Set device settings using API."""
|
||||
values = {}
|
||||
|
||||
for attr in [ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE,
|
||||
ATTR_OPERATION_MODE]:
|
||||
value = settings.get(attr)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
daikin_attr = HA_ATTR_TO_DAIKIN.get(attr)
|
||||
if daikin_attr is not None:
|
||||
if value.title() in self._list[attr]:
|
||||
values[daikin_attr] = value.lower()
|
||||
else:
|
||||
_LOGGER.error("Invalid value %s for %s", attr, value)
|
||||
|
||||
# temperature
|
||||
elif attr == ATTR_TEMPERATURE:
|
||||
try:
|
||||
values['stemp'] = str(int(value))
|
||||
except ValueError:
|
||||
_LOGGER.error("Invalid temperature %s", value)
|
||||
|
||||
if values:
|
||||
self._force_refresh = True
|
||||
self._api.device.set(values)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this AC."""
|
||||
return "{}.{}".format(self.__class__, self._api.ip_address)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the thermostat, if any."""
|
||||
return self._api.name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.get(ATTR_CURRENT_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.get(ATTR_TARGET_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return 1
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
self.set(kwargs)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self.get(ATTR_OPERATION_MODE)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._list.get(ATTR_OPERATION_MODE)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set HVAC mode."""
|
||||
self.set({ATTR_OPERATION_MODE: operation_mode})
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.get(ATTR_FAN_MODE)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set fan mode."""
|
||||
self.set({ATTR_FAN_MODE: fan})
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return self._list.get(ATTR_FAN_MODE)
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.get(ATTR_SWING_MODE)
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target temperature."""
|
||||
self.set({ATTR_SWING_MODE: swing_mode})
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return self._list.get(ATTR_SWING_MODE)
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._api.update(no_throttle=self._force_refresh)
|
||||
self._force_refresh = False
|
||||
@@ -9,14 +9,15 @@ from homeassistant.components.climate import (
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||
SUPPORT_ON_OFF)
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY |
|
||||
SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT |
|
||||
SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_ON_OFF)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -56,6 +57,7 @@ class DemoClimate(ClimateDevice):
|
||||
self._swing_list = ['Auto', '1', '2', '3', 'Off']
|
||||
self._target_temperature_high = target_temp_high
|
||||
self._target_temperature_low = target_temp_low
|
||||
self._on = True
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@@ -132,6 +134,11 @@ class DemoClimate(ClimateDevice):
|
||||
"""Return true if aux heat is on."""
|
||||
return self._aux
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the device is on."""
|
||||
return self._on
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
@@ -206,3 +213,13 @@ class DemoClimate(ClimateDevice):
|
||||
"""Turn auxiliary heater off."""
|
||||
self._aux = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on."""
|
||||
self._on = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off."""
|
||||
self._on = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.ecobee/
|
||||
"""
|
||||
import logging
|
||||
from os import path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -17,7 +16,6 @@ from homeassistant.components.climate import (
|
||||
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_CONFIGURING = {}
|
||||
@@ -96,17 +94,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
thermostat.schedule_update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service,
|
||||
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
|
||||
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service,
|
||||
descriptions.get(SERVICE_RESUME_PROGRAM),
|
||||
schema=RESUME_PROGRAM_SCHEMA)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Support for Rheem EcoNet water heaters.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.econet/
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN,
|
||||
PLATFORM_SCHEMA,
|
||||
STATE_ECO, STATE_GAS, STATE_ELECTRIC,
|
||||
STATE_HEAT_PUMP, STATE_HIGH_DEMAND,
|
||||
STATE_OFF, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE,
|
||||
ClimateDevice)
|
||||
from homeassistant.const import (ATTR_ENTITY_ID,
|
||||
CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyeconet==0.0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_VACATION_START = 'next_vacation_start_date'
|
||||
ATTR_VACATION_END = 'next_vacation_end_date'
|
||||
ATTR_ON_VACATION = 'on_vacation'
|
||||
ATTR_TODAYS_ENERGY_USAGE = 'todays_energy_usage'
|
||||
ATTR_IN_USE = 'in_use'
|
||||
|
||||
ATTR_START_DATE = 'start_date'
|
||||
ATTR_END_DATE = 'end_date'
|
||||
|
||||
SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
|
||||
|
||||
SERVICE_ADD_VACATION = 'econet_add_vacation'
|
||||
SERVICE_DELETE_VACATION = 'econet_delete_vacation'
|
||||
|
||||
ADD_VACATION_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_START_DATE): cv.positive_int,
|
||||
vol.Required(ATTR_END_DATE): cv.positive_int,
|
||||
})
|
||||
|
||||
DELETE_VACATION_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
ECONET_DATA = 'econet'
|
||||
|
||||
HA_STATE_TO_ECONET = {
|
||||
STATE_ECO: 'Energy Saver',
|
||||
STATE_ELECTRIC: 'Electric',
|
||||
STATE_HEAT_PUMP: 'Heat Pump',
|
||||
STATE_GAS: 'gas',
|
||||
STATE_HIGH_DEMAND: 'High Demand',
|
||||
STATE_OFF: 'Off',
|
||||
}
|
||||
|
||||
ECONET_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_ECONET.items()}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the EcoNet water heaters."""
|
||||
from pyeconet.api import PyEcoNet
|
||||
|
||||
hass.data[ECONET_DATA] = {}
|
||||
hass.data[ECONET_DATA]['water_heaters'] = []
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
econet = PyEcoNet(username, password)
|
||||
water_heaters = econet.get_water_heaters()
|
||||
hass_water_heaters = [
|
||||
EcoNetWaterHeater(water_heater) for water_heater in water_heaters]
|
||||
add_devices(hass_water_heaters)
|
||||
hass.data[ECONET_DATA]['water_heaters'].extend(hass_water_heaters)
|
||||
|
||||
def service_handle(service):
|
||||
"""Handler for services."""
|
||||
entity_ids = service.data.get('entity_id')
|
||||
all_heaters = hass.data[ECONET_DATA]['water_heaters']
|
||||
_heaters = [
|
||||
x for x in all_heaters
|
||||
if not entity_ids or x.entity_id in entity_ids]
|
||||
|
||||
for _water_heater in _heaters:
|
||||
if service.service == SERVICE_ADD_VACATION:
|
||||
start = service.data.get(ATTR_START_DATE)
|
||||
end = service.data.get(ATTR_END_DATE)
|
||||
_water_heater.add_vacation(start, end)
|
||||
if service.service == SERVICE_DELETE_VACATION:
|
||||
for vacation in _water_heater.water_heater.vacations:
|
||||
vacation.delete()
|
||||
|
||||
_water_heater.schedule_update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_ADD_VACATION,
|
||||
service_handle,
|
||||
schema=ADD_VACATION_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_DELETE_VACATION,
|
||||
service_handle,
|
||||
schema=DELETE_VACATION_SCHEMA)
|
||||
|
||||
|
||||
class EcoNetWaterHeater(ClimateDevice):
|
||||
"""Representation of an EcoNet water heater."""
|
||||
|
||||
def __init__(self, water_heater):
|
||||
"""Initialize the water heater."""
|
||||
self.water_heater = water_heater
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
return self.water_heater.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if the the device is online or not."""
|
||||
return self.water_heater.is_connected
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {}
|
||||
vacations = self.water_heater.get_vacations()
|
||||
if vacations:
|
||||
data[ATTR_VACATION_START] = vacations[0].start_date
|
||||
data[ATTR_VACATION_END] = vacations[0].end_date
|
||||
data[ATTR_ON_VACATION] = self.water_heater.is_on_vacation
|
||||
todays_usage = self.water_heater.total_usage_for_today
|
||||
if todays_usage:
|
||||
data[ATTR_TODAYS_ENERGY_USAGE] = todays_usage
|
||||
data[ATTR_IN_USE] = self.water_heater.in_use
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""
|
||||
Return current operation as one of the following.
|
||||
|
||||
["eco", "heat_pump",
|
||||
"high_demand", "electric_only"]
|
||||
"""
|
||||
current_op = ECONET_STATE_TO_HA.get(self.water_heater.mode)
|
||||
return current_op
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
op_list = []
|
||||
modes = self.water_heater.supported_modes
|
||||
for mode in modes:
|
||||
ha_mode = ECONET_STATE_TO_HA.get(mode)
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invalid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS_HEATER
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
if target_temp is not None:
|
||||
self.water_heater.set_target_set_point(target_temp)
|
||||
else:
|
||||
_LOGGER.error("A target temperature must be provided.")
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
op_mode_to_set = HA_STATE_TO_ECONET.get(operation_mode)
|
||||
if op_mode_to_set is not None:
|
||||
self.water_heater.set_mode(op_mode_to_set)
|
||||
else:
|
||||
_LOGGER.error("An operation mode must be provided.")
|
||||
|
||||
def add_vacation(self, start, end):
|
||||
"""Add a vacation to this water heater."""
|
||||
if not start:
|
||||
start = datetime.datetime.now()
|
||||
else:
|
||||
start = datetime.datetime.fromtimestamp(start)
|
||||
end = datetime.datetime.fromtimestamp(end)
|
||||
self.water_heater.set_vacation_mode(start, end)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest date."""
|
||||
self.water_heater.update_state()
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.water_heater.set_point
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self.water_heater.min_set_point
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self.water_heater.max_set_point
|
||||
@@ -12,8 +12,9 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
|
||||
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
|
||||
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
@@ -29,6 +30,7 @@ DEPENDENCIES = ['switch', 'sensor']
|
||||
|
||||
DEFAULT_TOLERANCE = 0.3
|
||||
DEFAULT_NAME = 'Generic Thermostat'
|
||||
DEFAULT_AWAY_TEMP = 16
|
||||
|
||||
CONF_HEATER = 'heater'
|
||||
CONF_SENSOR = 'target_sensor'
|
||||
@@ -40,8 +42,10 @@ CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_COLD_TOLERANCE = 'cold_tolerance'
|
||||
CONF_HOT_TOLERANCE = 'hot_tolerance'
|
||||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
|
||||
CONF_AWAY_TEMP = 'away_temp'
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HEATER): cv.entity_id,
|
||||
@@ -58,6 +62,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_KEEP_ALIVE): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_INITIAL_OPERATION_MODE):
|
||||
vol.In([STATE_AUTO, STATE_OFF]),
|
||||
vol.Optional(CONF_AWAY_TEMP,
|
||||
default=DEFAULT_AWAY_TEMP): vol.Coerce(float)
|
||||
})
|
||||
|
||||
|
||||
@@ -75,11 +83,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
cold_tolerance = config.get(CONF_COLD_TOLERANCE)
|
||||
hot_tolerance = config.get(CONF_HOT_TOLERANCE)
|
||||
keep_alive = config.get(CONF_KEEP_ALIVE)
|
||||
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
|
||||
away_temp = config.get(CONF_AWAY_TEMP)
|
||||
|
||||
async_add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration, cold_tolerance,
|
||||
hot_tolerance, keep_alive)])
|
||||
hot_tolerance, keep_alive, initial_operation_mode, away_temp)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
@@ -87,7 +97,8 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
cold_tolerance, hot_tolerance, keep_alive):
|
||||
cold_tolerance, hot_tolerance, keep_alive,
|
||||
initial_operation_mode, away_temp):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@@ -97,14 +108,27 @@ class GenericThermostat(ClimateDevice):
|
||||
self._cold_tolerance = cold_tolerance
|
||||
self._hot_tolerance = hot_tolerance
|
||||
self._keep_alive = keep_alive
|
||||
self._enabled = True
|
||||
|
||||
self._initial_operation_mode = initial_operation_mode
|
||||
self._saved_target_temp = target_temp if target_temp is not None \
|
||||
else away_temp
|
||||
if self.ac_mode:
|
||||
self._current_operation = STATE_COOL
|
||||
self._operation_list = [STATE_COOL, STATE_OFF]
|
||||
else:
|
||||
self._current_operation = STATE_HEAT
|
||||
self._operation_list = [STATE_HEAT, STATE_OFF]
|
||||
if initial_operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
else:
|
||||
self._enabled = True
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = target_temp
|
||||
self._unit = hass.config.units.temperature_unit
|
||||
self._away_temp = away_temp
|
||||
self._is_away = False
|
||||
|
||||
async_track_state_change(
|
||||
hass, sensor_entity_id, self._async_sensor_changed)
|
||||
@@ -115,20 +139,45 @@ class GenericThermostat(ClimateDevice):
|
||||
async_track_time_interval(
|
||||
hass, self._async_keep_alive, self._keep_alive)
|
||||
|
||||
sensor_state = hass.states.get(sensor_entity_id)
|
||||
if sensor_state:
|
||||
self._async_update_temp(sensor_state)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
# If we have an old state and no target temp, restore
|
||||
if self._target_temp is None:
|
||||
old_state = yield from async_get_last_state(self.hass,
|
||||
self.entity_id)
|
||||
if old_state is not None:
|
||||
self._target_temp = float(
|
||||
old_state.attributes[ATTR_TEMPERATURE])
|
||||
# Check If we have an old state
|
||||
old_state = yield from async_get_last_state(self.hass,
|
||||
self.entity_id)
|
||||
if old_state is not None:
|
||||
# If we have no initial temperature, restore
|
||||
if self._target_temp is None:
|
||||
# If we have a previously saved temperature
|
||||
if old_state.attributes[ATTR_TEMPERATURE] is None:
|
||||
if self.ac_mode:
|
||||
self._target_temp = self.max_temp
|
||||
else:
|
||||
self._target_temp = self.min_temp
|
||||
_LOGGER.warning('Undefined target temperature, \
|
||||
falling back to %s', self._target_temp)
|
||||
else:
|
||||
self._target_temp = float(
|
||||
old_state.attributes[ATTR_TEMPERATURE])
|
||||
self._is_away = True if str(
|
||||
old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON else False
|
||||
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
|
||||
self._current_operation = STATE_OFF
|
||||
self._enabled = False
|
||||
if self._initial_operation_mode is None:
|
||||
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
|
||||
self._enabled = False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
if self._is_device_active:
|
||||
return self.current_operation
|
||||
else:
|
||||
if self._enabled:
|
||||
return STATE_IDLE
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -152,15 +201,8 @@ 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
|
||||
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
"""Return current operation."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
@@ -170,14 +212,20 @@ class GenericThermostat(ClimateDevice):
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [STATE_AUTO, STATE_OFF]
|
||||
return self._operation_list
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_AUTO:
|
||||
if operation_mode == STATE_HEAT:
|
||||
self._current_operation = STATE_HEAT
|
||||
self._enabled = True
|
||||
self._async_control_heating()
|
||||
elif operation_mode == STATE_COOL:
|
||||
self._current_operation = STATE_COOL
|
||||
self._enabled = True
|
||||
self._async_control_heating()
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._current_operation = STATE_OFF
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
self._heater_turn_off()
|
||||
@@ -237,7 +285,7 @@ class GenericThermostat(ClimateDevice):
|
||||
@callback
|
||||
def _async_keep_alive(self, time):
|
||||
"""Call at constant intervals for keep-alive purposes."""
|
||||
if self.current_operation in [STATE_COOL, STATE_HEAT]:
|
||||
if self._is_device_active:
|
||||
self._heater_turn_on()
|
||||
else:
|
||||
self._heater_turn_off()
|
||||
@@ -332,3 +380,23 @@ class GenericThermostat(ClimateDevice):
|
||||
data = {ATTR_ENTITY_ID: self.heater_entity_id}
|
||||
self.hass.async_add_job(
|
||||
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data))
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._is_away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on by setting it on away hold indefinitely."""
|
||||
self._is_away = True
|
||||
self._saved_target_temp = self._target_temp
|
||||
self._target_temp = self._away_temp
|
||||
self._async_control_heating()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self._is_away = False
|
||||
self._target_temp = self._saved_target_temp
|
||||
self._async_control_heating()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.hive/
|
||||
"""
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
from homeassistant.components.hive import DATA_HIVE
|
||||
|
||||
@@ -16,7 +16,9 @@ HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
|
||||
HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL',
|
||||
STATE_ON: 'ON', STATE_OFF: 'OFF'}
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AUX_HEAT)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -134,6 +136,43 @@ class HiveClimateEntity(ClimateDevice):
|
||||
for entity in self.session.entities:
|
||||
entity.handle_update(self.data_updatesource)
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if auxiliary heater is on."""
|
||||
boost_status = None
|
||||
if self.device_type == "Heating":
|
||||
boost_status = self.session.heating.get_boost(self.node_id)
|
||||
elif self.device_type == "HotWater":
|
||||
boost_status = self.session.hotwater.get_boost(self.node_id)
|
||||
return boost_status == "ON"
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxiliary heater on."""
|
||||
target_boost_time = 30
|
||||
if self.device_type == "Heating":
|
||||
curtemp = self.session.heating.current_temperature(self.node_id)
|
||||
curtemp = round(curtemp * 2) / 2
|
||||
target_boost_temperature = curtemp + 0.5
|
||||
self.session.heating.turn_boost_on(self.node_id,
|
||||
target_boost_time,
|
||||
target_boost_temperature)
|
||||
elif self.device_type == "HotWater":
|
||||
self.session.hotwater.turn_boost_on(self.node_id,
|
||||
target_boost_time)
|
||||
|
||||
for entity in self.session.entities:
|
||||
entity.handle_update(self.data_updatesource)
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxiliary heater off."""
|
||||
if self.device_type == "Heating":
|
||||
self.session.heating.turn_boost_off(self.node_id)
|
||||
elif self.device_type == "HotWater":
|
||||
self.session.hotwater.turn_boost_off(self.node_id)
|
||||
|
||||
for entity in self.session.entities:
|
||||
entity.handle_update(self.data_updatesource)
|
||||
|
||||
def update(self):
|
||||
"""Update all Node data frome Hive."""
|
||||
self.session.core.update_data(self.node_id)
|
||||
|
||||
@@ -8,7 +8,8 @@ import logging
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.components.homematic import (
|
||||
HMDevice, ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT)
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
@@ -39,6 +40,7 @@ HM_HUMI_MAP = [
|
||||
]
|
||||
|
||||
HM_CONTROL_MODE = 'CONTROL_MODE'
|
||||
HM_IP_CONTROL_MODE = 'SET_POINT_MODE'
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
@@ -75,11 +77,25 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
if HM_CONTROL_MODE not in self._data:
|
||||
return None
|
||||
|
||||
# read state and search
|
||||
for mode, state in HM_STATE_MAP.items():
|
||||
code = getattr(self._hmdevice, mode, 0)
|
||||
if self._data.get('CONTROL_MODE') == code:
|
||||
return state
|
||||
set_point_mode = self._data.get('SET_POINT_MODE', -1)
|
||||
control_mode = self._data.get('CONTROL_MODE', -1)
|
||||
boost_mode = self._data.get('BOOST_MODE', False)
|
||||
|
||||
# boost mode is active
|
||||
if boost_mode:
|
||||
return STATE_BOOST
|
||||
|
||||
# HM ip etrv 2 uses the set_point_mode to say if its
|
||||
# auto or manual
|
||||
elif not set_point_mode == -1:
|
||||
code = set_point_mode
|
||||
# Other devices use the control_mode
|
||||
else:
|
||||
code = control_mode
|
||||
|
||||
# get the name of the mode
|
||||
name = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][code]
|
||||
return name.lower()
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@@ -125,6 +141,7 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
if state == operation_mode:
|
||||
code = getattr(self._hmdevice, mode, 0)
|
||||
self._hmdevice.MODE = code
|
||||
return
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
@@ -141,7 +158,8 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
self._state = next(iter(self._hmdevice.WRITENODE.keys()))
|
||||
self._data[self._state] = STATE_UNKNOWN
|
||||
|
||||
if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
|
||||
if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE or \
|
||||
HM_IP_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data[HM_CONTROL_MODE] = STATE_UNKNOWN
|
||||
|
||||
for node in self._hmdevice.SENSORNODE.keys():
|
||||
|
||||
@@ -159,6 +159,11 @@ class KNXClimate(ClimateDevice):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.hass.data[DATA_KNX].connected
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed within KNX."""
|
||||
|
||||
@@ -20,8 +20,9 @@ from homeassistant.components.climate import (
|
||||
SUPPORT_AUX_HEAT)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
|
||||
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN,
|
||||
MQTT_BASE_PLATFORM_SCHEMA)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH)
|
||||
@@ -93,7 +94,7 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
|
||||
})
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -134,19 +135,25 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
STATE_OFF, STATE_OFF, False,
|
||||
config.get(CONF_SEND_IF_OFF),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF))
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE))
|
||||
])
|
||||
|
||||
|
||||
class MqttClimate(ClimateDevice):
|
||||
class MqttClimate(MqttAvailability, ClimateDevice):
|
||||
"""Representation of a demo climate device."""
|
||||
|
||||
def __init__(self, hass, name, topic, qos, retain, mode_list,
|
||||
fan_mode_list, swing_mode_list, target_temperature, away,
|
||||
hold, current_fan_mode, current_swing_mode,
|
||||
current_operation, aux, send_if_off, payload_on,
|
||||
payload_off):
|
||||
payload_off, availability_topic, payload_available,
|
||||
payload_not_available):
|
||||
"""Initialize the climate device."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
@@ -169,8 +176,11 @@ class MqttClimate(ClimateDevice):
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Handle being added to home assistant."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def handle_current_temp_received(topic, payload, qos):
|
||||
"""Handle current temperature coming via MQTT."""
|
||||
|
||||
Executable → Regular
Executable → Regular
-5
@@ -79,11 +79,6 @@ class NetatmoThermostat(ClimateDevice):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Support for NuHeat thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.nuheat/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice,
|
||||
DOMAIN,
|
||||
SUPPORT_HOLD_MODE,
|
||||
SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
STATE_AUTO,
|
||||
STATE_HEAT,
|
||||
STATE_IDLE)
|
||||
from homeassistant.components.nuheat import DOMAIN as NUHEAT_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TEMPERATURE,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DEPENDENCIES = ["nuheat"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ICON = "mdi:thermometer"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
|
||||
# Hold modes
|
||||
MODE_AUTO = STATE_AUTO # Run device schedule
|
||||
MODE_HOLD_TEMPERATURE = "temperature"
|
||||
MODE_TEMPORARY_HOLD = "temporary_temperature"
|
||||
|
||||
OPERATION_LIST = [STATE_HEAT, STATE_IDLE]
|
||||
|
||||
SCHEDULE_HOLD = 3
|
||||
SCHEDULE_RUN = 1
|
||||
SCHEDULE_TEMPORARY_HOLD = 2
|
||||
|
||||
SERVICE_RESUME_PROGRAM = "nuheat_resume_program"
|
||||
|
||||
RESUME_PROGRAM_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids
|
||||
})
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_HOLD_MODE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the NuHeat thermostat(s)."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
temperature_unit = hass.config.units.temperature_unit
|
||||
api, serial_numbers = hass.data[NUHEAT_DOMAIN]
|
||||
thermostats = [
|
||||
NuHeatThermostat(api, serial_number, temperature_unit)
|
||||
for serial_number in serial_numbers
|
||||
]
|
||||
add_devices(thermostats, True)
|
||||
|
||||
def resume_program_set_service(service):
|
||||
"""Resume the program on the target thermostats."""
|
||||
entity_id = service.data.get(ATTR_ENTITY_ID)
|
||||
if entity_id:
|
||||
target_thermostats = [device for device in thermostats
|
||||
if device.entity_id in entity_id]
|
||||
else:
|
||||
target_thermostats = thermostats
|
||||
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.resume_program()
|
||||
|
||||
thermostat.schedule_update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service,
|
||||
schema=RESUME_PROGRAM_SCHEMA)
|
||||
|
||||
|
||||
class NuHeatThermostat(ClimateDevice):
|
||||
"""Representation of a NuHeat Thermostat."""
|
||||
|
||||
def __init__(self, api, serial_number, temperature_unit):
|
||||
"""Initialize the thermostat."""
|
||||
self._thermostat = api.get_thermostat(serial_number)
|
||||
self._temperature_unit = temperature_unit
|
||||
self._force_update = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the thermostat."""
|
||||
return self._thermostat.room
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self._temperature_unit == "C":
|
||||
return TEMP_CELSIUS
|
||||
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if self._temperature_unit == "C":
|
||||
return self._thermostat.celsius
|
||||
|
||||
return self._thermostat.fahrenheit
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation. ie. heat, idle."""
|
||||
if self._thermostat.heating:
|
||||
return STATE_HEAT
|
||||
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum supported temperature for the thermostat."""
|
||||
if self._temperature_unit == "C":
|
||||
return self._thermostat.min_celsius
|
||||
|
||||
return self._thermostat.min_fahrenheit
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum supported temperature for the thermostat."""
|
||||
if self._temperature_unit == "C":
|
||||
return self._thermostat.max_celsius
|
||||
|
||||
return self._thermostat.max_fahrenheit
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the currently programmed temperature."""
|
||||
if self._temperature_unit == "C":
|
||||
return self._thermostat.target_celsius
|
||||
|
||||
return self._thermostat.target_fahrenheit
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
schedule_mode = self._thermostat.schedule_mode
|
||||
if schedule_mode == SCHEDULE_RUN:
|
||||
return MODE_AUTO
|
||||
|
||||
if schedule_mode == SCHEDULE_HOLD:
|
||||
return MODE_HOLD_TEMPERATURE
|
||||
|
||||
if schedule_mode == SCHEDULE_TEMPORARY_HOLD:
|
||||
return MODE_TEMPORARY_HOLD
|
||||
|
||||
return MODE_AUTO
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return list of possible operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
def resume_program(self):
|
||||
"""Resume the thermostat's programmed schedule."""
|
||||
self._thermostat.resume_schedule()
|
||||
self._force_update = True
|
||||
|
||||
def set_hold_mode(self, hold_mode, **kwargs):
|
||||
"""Update the hold mode of the thermostat."""
|
||||
if hold_mode == MODE_AUTO:
|
||||
schedule_mode = SCHEDULE_RUN
|
||||
|
||||
if hold_mode == MODE_HOLD_TEMPERATURE:
|
||||
schedule_mode = SCHEDULE_HOLD
|
||||
|
||||
if hold_mode == MODE_TEMPORARY_HOLD:
|
||||
schedule_mode = SCHEDULE_TEMPORARY_HOLD
|
||||
|
||||
self._thermostat.schedule_mode = schedule_mode
|
||||
self._force_update = True
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set a new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if self._temperature_unit == "C":
|
||||
self._thermostat.target_celsius = temperature
|
||||
else:
|
||||
self._thermostat.target_fahrenheit = temperature
|
||||
|
||||
_LOGGER.debug(
|
||||
"Setting NuHeat thermostat temperature to %s %s",
|
||||
temperature, self.temperature_unit)
|
||||
|
||||
self._force_update = True
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state from the thermostat."""
|
||||
if self._force_update:
|
||||
self._throttled_update(no_throttle=True)
|
||||
self._force_update = False
|
||||
else:
|
||||
self._throttled_update()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _throttled_update(self, **kwargs):
|
||||
"""Get the latest state from the thermostat with a throttle."""
|
||||
self._thermostat.get_data()
|
||||
@@ -13,37 +13,49 @@ import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
ATTR_ENTITY_ID, ATTR_STATE, ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID,
|
||||
STATE_ON, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA,
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_SWING_MODE,
|
||||
SUPPORT_AUX_HEAT)
|
||||
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
|
||||
SUPPORT_ON_OFF)
|
||||
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
|
||||
|
||||
REQUIREMENTS = ['pysensibo==1.0.1']
|
||||
REQUIREMENTS = ['pysensibo==1.0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ALL = 'all'
|
||||
TIMEOUT = 10
|
||||
|
||||
SERVICE_ASSUME_STATE = 'sensibo_assume_state'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_ID, default=ALL): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
ASSUME_STATE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_STATE): cv.string,
|
||||
})
|
||||
|
||||
_FETCH_FIELDS = ','.join([
|
||||
'room{name}', 'measurements', 'remoteCapabilities',
|
||||
'acState', 'connectionStatus{isAlive}', 'temperatureUnit'])
|
||||
_INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_SWING_MODE |
|
||||
SUPPORT_AUX_HEAT)
|
||||
FIELD_TO_FLAG = {
|
||||
'fanLevel': SUPPORT_FAN_MODE,
|
||||
'mode': SUPPORT_OPERATION_MODE,
|
||||
'swing': SUPPORT_SWING_MODE,
|
||||
'targetTemperature': SUPPORT_TARGET_TEMPERATURE,
|
||||
'on': SUPPORT_ON_OFF,
|
||||
}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -68,6 +80,28 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_assume_state(service):
|
||||
"""Set state according to external service call.."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
if entity_ids:
|
||||
target_climate = [device for device in devices
|
||||
if device.entity_id in entity_ids]
|
||||
else:
|
||||
target_climate = devices
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_assume_state(
|
||||
service.data.get(ATTR_STATE))
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ASSUME_STATE, async_assume_state,
|
||||
schema=ASSUME_STATE_SCHEMA)
|
||||
|
||||
|
||||
class SensiboClimate(ClimateDevice):
|
||||
"""Representation of a Sensibo device."""
|
||||
@@ -80,12 +114,13 @@ class SensiboClimate(ClimateDevice):
|
||||
"""
|
||||
self._client = client
|
||||
self._id = data['id']
|
||||
self._external_state = None
|
||||
self._do_update(data)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
return self._supported_features
|
||||
|
||||
def _do_update(self, data):
|
||||
self._name = data['room']['name']
|
||||
@@ -106,6 +141,15 @@ class SensiboClimate(ClimateDevice):
|
||||
else:
|
||||
self._temperature_unit = self.unit_of_measurement
|
||||
self._temperatures_list = []
|
||||
self._supported_features = 0
|
||||
for key in self._ac_states:
|
||||
if key in FIELD_TO_FLAG:
|
||||
self._supported_features |= FIELD_TO_FLAG[key]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
return self._external_state or super().state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -188,7 +232,7 @@ class SensiboClimate(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
def is_on(self):
|
||||
"""Return true if AC is on."""
|
||||
return self._ac_states['on']
|
||||
|
||||
@@ -196,13 +240,13 @@ class SensiboClimate(ClimateDevice):
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._temperatures_list[0] \
|
||||
if len(self._temperatures_list) else super.min_temp()
|
||||
if len(self._temperatures_list) else super().min_temp()
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._temperatures_list[-1] \
|
||||
if len(self._temperatures_list) else super.max_temp()
|
||||
if len(self._temperatures_list) else super().max_temp()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
@@ -226,42 +270,62 @@ class SensiboClimate(ClimateDevice):
|
||||
|
||||
with async_timeout.timeout(TIMEOUT):
|
||||
yield from self._client.async_set_ac_state_property(
|
||||
self._id, 'targetTemperature', temperature)
|
||||
self._id, 'targetTemperature', temperature, self._ac_states)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
with async_timeout.timeout(TIMEOUT):
|
||||
yield from self._client.async_set_ac_state_property(
|
||||
self._id, 'fanLevel', fan)
|
||||
self._id, 'fanLevel', fan, self._ac_states)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
with async_timeout.timeout(TIMEOUT):
|
||||
yield from self._client.async_set_ac_state_property(
|
||||
self._id, 'mode', operation_mode)
|
||||
self._id, 'mode', operation_mode, self._ac_states)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
with async_timeout.timeout(TIMEOUT):
|
||||
yield from self._client.async_set_ac_state_property(
|
||||
self._id, 'swing', swing_mode)
|
||||
self._id, 'swing', swing_mode, self._ac_states)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_aux_heat_on(self):
|
||||
def async_on(self):
|
||||
"""Turn Sensibo unit on."""
|
||||
with async_timeout.timeout(TIMEOUT):
|
||||
yield from self._client.async_set_ac_state_property(
|
||||
self._id, 'on', True)
|
||||
self._id, 'on', True, self._ac_states)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_aux_heat_off(self):
|
||||
def async_off(self):
|
||||
"""Turn Sensibo unit on."""
|
||||
with async_timeout.timeout(TIMEOUT):
|
||||
yield from self._client.async_set_ac_state_property(
|
||||
self._id, 'on', False)
|
||||
self._id, 'on', False, self._ac_states)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_assume_state(self, state):
|
||||
"""Set external state."""
|
||||
change_needed = (state != STATE_OFF and not self.is_on) \
|
||||
or (state == STATE_OFF and self.is_on)
|
||||
if change_needed:
|
||||
with async_timeout.timeout(TIMEOUT):
|
||||
yield from self._client.async_set_ac_state_property(
|
||||
self._id,
|
||||
'on',
|
||||
state != STATE_OFF, # value
|
||||
self._ac_states,
|
||||
True # assumed_state
|
||||
)
|
||||
|
||||
if state in [STATE_ON, STATE_OFF]:
|
||||
self._external_state = None
|
||||
else:
|
||||
self._external_state = state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
|
||||
@@ -80,7 +80,22 @@ set_swing_mode:
|
||||
example: 'climate.nest'
|
||||
swing_mode:
|
||||
description: New value of swing mode.
|
||||
example: 1
|
||||
example:
|
||||
|
||||
turn_on:
|
||||
description: Turn climate device on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
turn_off:
|
||||
description: Turn climate device off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
ecobee_set_fan_min_on_time:
|
||||
description: Set the minimum fan on time.
|
||||
fields:
|
||||
@@ -100,3 +115,40 @@ ecobee_resume_program:
|
||||
resume_all:
|
||||
description: Resume all events and return to the scheduled program. This default to false which removes only the top event.
|
||||
example: true
|
||||
|
||||
nuheat_resume_program:
|
||||
description: Resume the programmed schedule.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
|
||||
econet_add_vacation:
|
||||
description: Add a vacation to your water heater.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.water_heater'
|
||||
start_date:
|
||||
description: The timestamp of when the vacation should start. (Optional, defaults to now)
|
||||
example: 1513186320
|
||||
end_date:
|
||||
description: The timestamp of when the vacation should end.
|
||||
example: 1513445520
|
||||
|
||||
econet_delete_vacation:
|
||||
description: Delete your existing vacation from your water heater.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.water_heater'
|
||||
|
||||
sensibo_assume_state:
|
||||
description: Set Sensibo device to external state.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change.
|
||||
example: 'climate.kitchen'
|
||||
state:
|
||||
description: State to set.
|
||||
example: 'idle'
|
||||
|
||||
@@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.tado/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS)
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
@@ -59,8 +59,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
climate_devices.append(create_climate_device(
|
||||
tado, hass, zone, zone['name'], zone['id']))
|
||||
device = create_climate_device(
|
||||
tado, hass, zone, zone['name'], zone['id'])
|
||||
if not device:
|
||||
continue
|
||||
climate_devices.append(device)
|
||||
|
||||
if climate_devices:
|
||||
add_devices(climate_devices, True)
|
||||
@@ -75,8 +78,11 @@ def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
|
||||
if ac_mode:
|
||||
temperatures = capabilities['HEAT']['temperatures']
|
||||
else:
|
||||
elif 'temperatures' in capabilities:
|
||||
temperatures = capabilities['temperatures']
|
||||
else:
|
||||
_LOGGER.debug("Received zone %s has no temperature; not adding", name)
|
||||
return
|
||||
|
||||
min_temp = float(temperatures['celsius']['min'])
|
||||
max_temp = float(temperatures['celsius']['max'])
|
||||
@@ -186,6 +192,11 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return true if away mode is on."""
|
||||
return self._is_away
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return PRECISION_TENTHS
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Platform for Roth Touchline heat pump controller.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/climate.touchline/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pytouchline==0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Touchline devices."""
|
||||
from pytouchline import PyTouchline
|
||||
host = config[CONF_HOST]
|
||||
py_touchline = PyTouchline()
|
||||
number_of_devices = int(py_touchline.get_number_of_devices(host))
|
||||
devices = []
|
||||
for device_id in range(0, number_of_devices):
|
||||
devices.append(Touchline(PyTouchline(device_id)))
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class Touchline(ClimateDevice):
|
||||
"""Representation of a Touchline device."""
|
||||
|
||||
def __init__(self, touchline_thermostat):
|
||||
"""Initialize the climate device."""
|
||||
self.unit = touchline_thermostat
|
||||
self._name = None
|
||||
self._current_temperature = None
|
||||
self._target_temperature = None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
def update(self):
|
||||
"""Update unit attributes."""
|
||||
self.unit.update()
|
||||
self._name = self.unit.get_name()
|
||||
self._current_temperature = self.unit.get_current_temperature()
|
||||
self._target_temperature = self.unit.get_target_temperature()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the climate device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.unit.set_target_temperature(self._target_temperature)
|
||||
@@ -32,8 +32,8 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up of Vera thermostats."""
|
||||
add_devices_callback(
|
||||
VeraThermostat(device, VERA_CONTROLLER) for
|
||||
device in VERA_DEVICES['climate'])
|
||||
VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
|
||||
device in hass.data[VERA_DEVICES]['climate'])
|
||||
|
||||
|
||||
class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
|
||||
Executable → Regular
@@ -5,48 +5,78 @@ import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE)
|
||||
from homeassistant.helpers import entityfilter
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.alexa import smart_home
|
||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||
from homeassistant.components.google_assistant import smart_home as ga_sh
|
||||
|
||||
from . import http_api, iot
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
|
||||
REQUIREMENTS = ['warrant==0.5.0']
|
||||
REQUIREMENTS = ['warrant==0.6.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ALEXA = 'alexa'
|
||||
CONF_ALEXA_FILTER = 'filter'
|
||||
CONF_GOOGLE_ACTIONS = 'google_actions'
|
||||
CONF_FILTER = 'filter'
|
||||
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
|
||||
CONF_RELAYER = 'relayer'
|
||||
CONF_USER_POOL_ID = 'user_pool_id'
|
||||
CONF_ALIASES = 'aliases'
|
||||
|
||||
MODE_DEV = 'development'
|
||||
DEFAULT_MODE = MODE_DEV
|
||||
DEFAULT_MODE = 'production'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ALEXA_SCHEMA = vol.Schema({
|
||||
CONF_ENTITY_CONFIG = 'entity_config'
|
||||
|
||||
ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(alexa_sh.CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(alexa_sh.CONF_DISPLAY_CATEGORIES): cv.string,
|
||||
vol.Optional(alexa_sh.CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
GOOGLE_ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_TYPE): vol.In(ga_sh.MAPPING_COMPONENT),
|
||||
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
|
||||
})
|
||||
|
||||
ASSISTANT_SCHEMA = vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_ALEXA_FILTER,
|
||||
CONF_FILTER,
|
||||
default=lambda: entityfilter.generate_filter([], [], [], [])
|
||||
): entityfilter.FILTER_SCHEMA,
|
||||
})
|
||||
|
||||
ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
|
||||
})
|
||||
|
||||
GACTIONS_SCHEMA = ASSISTANT_SCHEMA.extend({
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: GOOGLE_ENTITY_SCHEMA}
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
|
||||
vol.In([MODE_DEV] + list(SERVERS)),
|
||||
# Change to optional when we include real servers
|
||||
vol.Required(CONF_COGNITO_CLIENT_ID): str,
|
||||
vol.Required(CONF_USER_POOL_ID): str,
|
||||
vol.Required(CONF_REGION): str,
|
||||
vol.Required(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
|
||||
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
|
||||
vol.Optional(CONF_USER_POOL_ID): str,
|
||||
vol.Optional(CONF_REGION): str,
|
||||
vol.Optional(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -55,22 +85,26 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
def async_setup(hass, config):
|
||||
"""Initialize the Home Assistant cloud."""
|
||||
if DOMAIN in config:
|
||||
kwargs = config[DOMAIN]
|
||||
kwargs = dict(config[DOMAIN])
|
||||
else:
|
||||
kwargs = {CONF_MODE: DEFAULT_MODE}
|
||||
|
||||
if CONF_ALEXA not in kwargs:
|
||||
kwargs[CONF_ALEXA] = ALEXA_SCHEMA({})
|
||||
alexa_conf = kwargs.pop(CONF_ALEXA, None) or ALEXA_SCHEMA({})
|
||||
|
||||
if CONF_GOOGLE_ACTIONS not in kwargs:
|
||||
kwargs[CONF_GOOGLE_ACTIONS] = GACTIONS_SCHEMA({})
|
||||
|
||||
kwargs[CONF_ALEXA] = alexa_sh.Config(
|
||||
should_expose=alexa_conf[CONF_FILTER],
|
||||
entity_config=alexa_conf.get(CONF_ENTITY_CONFIG),
|
||||
)
|
||||
|
||||
kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA])
|
||||
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
||||
|
||||
@asyncio.coroutine
|
||||
def init_cloud(event):
|
||||
"""Initialize connection."""
|
||||
yield from cloud.initialize()
|
||||
success = yield from cloud.initialize()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, init_cloud)
|
||||
if not success:
|
||||
return False
|
||||
|
||||
yield from http_api.async_setup(hass)
|
||||
return True
|
||||
@@ -79,12 +113,16 @@ def async_setup(hass, config):
|
||||
class Cloud:
|
||||
"""Store the configuration of the cloud connection."""
|
||||
|
||||
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
|
||||
region=None, relayer=None, alexa=None):
|
||||
def __init__(self, hass, mode, alexa, google_actions,
|
||||
cognito_client_id=None, user_pool_id=None, region=None,
|
||||
relayer=None):
|
||||
"""Create an instance of Cloud."""
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
self.alexa_config = alexa
|
||||
self._google_actions = google_actions
|
||||
self._gactions_config = None
|
||||
self.jwt_keyset = None
|
||||
self.id_token = None
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
@@ -104,11 +142,6 @@ class Cloud:
|
||||
self.region = info['region']
|
||||
self.relayer = info['relayer']
|
||||
|
||||
@property
|
||||
def cognito_email_based(self):
|
||||
"""Return if cognito is email based."""
|
||||
return not self.user_pool_id.endswith('GmV')
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
"""Get if cloud is logged in."""
|
||||
@@ -117,10 +150,6 @@ class Cloud:
|
||||
@property
|
||||
def subscription_expired(self):
|
||||
"""Return a boolen if the subscription has expired."""
|
||||
# For now, don't enforce subscriptions to exist
|
||||
if 'custom:sub-exp' not in self.claims:
|
||||
return False
|
||||
|
||||
return dt_util.utcnow() > self.expiration_date
|
||||
|
||||
@property
|
||||
@@ -132,37 +161,44 @@ class Cloud:
|
||||
|
||||
@property
|
||||
def claims(self):
|
||||
"""Get the claims from the id token."""
|
||||
from jose import jwt
|
||||
return jwt.get_unverified_claims(self.id_token)
|
||||
"""Return the claims from the id token."""
|
||||
return self._decode_claims(self.id_token)
|
||||
|
||||
@property
|
||||
def user_info_path(self):
|
||||
"""Get path to the stored auth."""
|
||||
return self.path('{}_auth.json'.format(self.mode))
|
||||
|
||||
@property
|
||||
def gactions_config(self):
|
||||
"""Return the Google Assistant config."""
|
||||
if self._gactions_config is None:
|
||||
conf = self._google_actions
|
||||
|
||||
def should_expose(entity):
|
||||
"""If an entity should be exposed."""
|
||||
return conf['filter'](entity.entity_id)
|
||||
|
||||
self._gactions_config = ga_sh.Config(
|
||||
should_expose=should_expose,
|
||||
agent_user_id=self.claims['cognito:username'],
|
||||
entity_config=conf.get(CONF_ENTITY_CONFIG),
|
||||
)
|
||||
|
||||
return self._gactions_config
|
||||
|
||||
@asyncio.coroutine
|
||||
def initialize(self):
|
||||
"""Initialize and load cloud info."""
|
||||
def load_config():
|
||||
"""Load the configuration."""
|
||||
# Ensure config dir exists
|
||||
path = self.hass.config.path(CONFIG_DIR)
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
jwt_success = yield from self._fetch_jwt_keyset()
|
||||
|
||||
user_info = self.user_info_path
|
||||
if os.path.isfile(user_info):
|
||||
with open(user_info, 'rt') as file:
|
||||
info = json.loads(file.read())
|
||||
self.id_token = info['id_token']
|
||||
self.access_token = info['access_token']
|
||||
self.refresh_token = info['refresh_token']
|
||||
if not jwt_success:
|
||||
return False
|
||||
|
||||
yield from self.hass.async_add_job(load_config)
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START,
|
||||
self._start_cloud)
|
||||
|
||||
if self.id_token is not None:
|
||||
yield from self.iot.connect()
|
||||
return True
|
||||
|
||||
def path(self, *parts):
|
||||
"""Get config path inside cloud dir.
|
||||
@@ -179,6 +215,7 @@ class Cloud:
|
||||
self.id_token = None
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
self._gactions_config = None
|
||||
|
||||
yield from self.hass.async_add_job(
|
||||
lambda: os.remove(self.user_info_path))
|
||||
@@ -191,3 +228,79 @@ class Cloud:
|
||||
'access_token': self.access_token,
|
||||
'refresh_token': self.refresh_token,
|
||||
}, indent=4))
|
||||
|
||||
def _start_cloud(self, event):
|
||||
"""Start the cloud component."""
|
||||
# Ensure config dir exists
|
||||
path = self.hass.config.path(CONFIG_DIR)
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
|
||||
user_info = self.user_info_path
|
||||
if not os.path.isfile(user_info):
|
||||
return
|
||||
|
||||
with open(user_info, 'rt') as file:
|
||||
info = json.loads(file.read())
|
||||
|
||||
# Validate tokens
|
||||
try:
|
||||
for token in 'id_token', 'access_token':
|
||||
self._decode_claims(info[token])
|
||||
except ValueError as err: # Raised when token is invalid
|
||||
_LOGGER.warning('Found invalid token %s: %s', token, err)
|
||||
return
|
||||
|
||||
self.id_token = info['id_token']
|
||||
self.access_token = info['access_token']
|
||||
self.refresh_token = info['refresh_token']
|
||||
|
||||
self.hass.add_job(self.iot.connect())
|
||||
|
||||
@asyncio.coroutine
|
||||
def _fetch_jwt_keyset(self):
|
||||
"""Fetch the JWT keyset for the Cognito instance."""
|
||||
session = async_get_clientsession(self.hass)
|
||||
url = ("https://cognito-idp.us-east-1.amazonaws.com/"
|
||||
"{}/.well-known/jwks.json".format(self.user_pool_id))
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
req = yield from session.get(url)
|
||||
self.jwt_keyset = yield from req.json()
|
||||
|
||||
return True
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
||||
_LOGGER.error("Error fetching Cognito keyset: %s", err)
|
||||
return False
|
||||
|
||||
def _decode_claims(self, token):
|
||||
"""Decode the claims in a token."""
|
||||
from jose import jwt, exceptions as jose_exceptions
|
||||
try:
|
||||
header = jwt.get_unverified_header(token)
|
||||
except jose_exceptions.JWTError as err:
|
||||
raise ValueError(str(err)) from None
|
||||
kid = header.get("kid")
|
||||
|
||||
if kid is None:
|
||||
raise ValueError('No kid in header')
|
||||
|
||||
# Locate the key for this kid
|
||||
key = None
|
||||
for key_dict in self.jwt_keyset["keys"]:
|
||||
if key_dict["kid"] == kid:
|
||||
key = key_dict
|
||||
break
|
||||
if not key:
|
||||
raise ValueError(
|
||||
"Unable to locate kid ({}) in keyset".format(kid))
|
||||
|
||||
try:
|
||||
return jwt.decode(
|
||||
token, key, audience=self.cognito_client_id, options={
|
||||
'verify_exp': False,
|
||||
})
|
||||
except jose_exceptions.JWTError as err:
|
||||
raise ValueError(str(err)) from None
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Package to communicate with the authentication API."""
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
|
||||
@@ -58,21 +57,16 @@ def _map_aws_exception(err):
|
||||
return ex(err.response['Error']['Message'])
|
||||
|
||||
|
||||
def _generate_username(email):
|
||||
"""Generate a username from an email address."""
|
||||
return hashlib.sha512(email.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def register(cloud, email, password):
|
||||
"""Register a new account."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(cloud)
|
||||
# Workaround for bug in Warrant. PR with fix:
|
||||
# https://github.com/capless/warrant/pull/82
|
||||
cognito.add_base_attributes()
|
||||
try:
|
||||
if cloud.cognito_email_based:
|
||||
cognito.register(email, password, email=email)
|
||||
else:
|
||||
cognito.register(_generate_username(email), password, email=email)
|
||||
cognito.register(email, password)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
@@ -83,11 +77,22 @@ def confirm_register(cloud, confirmation_code, email):
|
||||
|
||||
cognito = _cognito(cloud)
|
||||
try:
|
||||
if cloud.cognito_email_based:
|
||||
cognito.confirm_sign_up(confirmation_code, email)
|
||||
else:
|
||||
cognito.confirm_sign_up(confirmation_code,
|
||||
_generate_username(email))
|
||||
cognito.confirm_sign_up(confirmation_code, email)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def resend_email_confirm(cloud, email):
|
||||
"""Resend email confirmation."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(cloud, username=email)
|
||||
|
||||
try:
|
||||
cognito.client.resend_confirmation_code(
|
||||
Username=email,
|
||||
ClientId=cognito.client_id
|
||||
)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
@@ -96,10 +101,7 @@ def forgot_password(cloud, email):
|
||||
"""Initiate forgotten password flow."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
if cloud.cognito_email_based:
|
||||
cognito = _cognito(cloud, username=email)
|
||||
else:
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
cognito = _cognito(cloud, username=email)
|
||||
|
||||
try:
|
||||
cognito.initiate_forgot_password()
|
||||
@@ -111,10 +113,7 @@ def confirm_forgot_password(cloud, confirmation_code, email, new_password):
|
||||
"""Confirm forgotten password code and change password."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
if cloud.cognito_email_based:
|
||||
cognito = _cognito(cloud, username=email)
|
||||
else:
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
cognito = _cognito(cloud, username=email)
|
||||
|
||||
try:
|
||||
cognito.confirm_forgot_password(confirmation_code, new_password)
|
||||
|
||||
@@ -4,13 +4,12 @@ CONFIG_DIR = '.cloud'
|
||||
REQUEST_TIMEOUT = 10
|
||||
|
||||
SERVERS = {
|
||||
# Example entry:
|
||||
# 'production': {
|
||||
# 'cognito_client_id': '',
|
||||
# 'user_pool_id': '',
|
||||
# 'region': '',
|
||||
# 'relayer': ''
|
||||
# }
|
||||
'production': {
|
||||
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
|
||||
'user_pool_id': 'us-east-1_87ll5WOP8',
|
||||
'region': 'us-east-1',
|
||||
'relayer': 'wss://cloud.hass.io:8000/websocket'
|
||||
}
|
||||
}
|
||||
|
||||
MESSAGE_EXPIRATION = """
|
||||
|
||||
@@ -23,6 +23,7 @@ def async_setup(hass):
|
||||
hass.http.register_view(CloudAccountView)
|
||||
hass.http.register_view(CloudRegisterView)
|
||||
hass.http.register_view(CloudConfirmRegisterView)
|
||||
hass.http.register_view(CloudResendConfirmView)
|
||||
hass.http.register_view(CloudForgotPasswordView)
|
||||
hass.http.register_view(CloudConfirmForgotPasswordView)
|
||||
|
||||
@@ -172,6 +173,29 @@ class CloudConfirmRegisterView(HomeAssistantView):
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudResendConfirmView(HomeAssistantView):
|
||||
"""Resend email confirmation code."""
|
||||
|
||||
url = '/api/cloud/resend_confirm'
|
||||
name = 'api:cloud:resend_confirm'
|
||||
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle resending confirm email code request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.resend_email_confirm, cloud, data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudForgotPasswordView(HomeAssistantView):
|
||||
"""View to start Forgot Password flow.."""
|
||||
|
||||
@@ -228,6 +252,6 @@ def _account_data(cloud):
|
||||
|
||||
return {
|
||||
'email': claims['email'],
|
||||
'sub_exp': claims.get('custom:sub-exp'),
|
||||
'sub_exp': claims['custom:sub-exp'],
|
||||
'cloud': cloud.iot.state,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import logging
|
||||
from aiohttp import hdrs, client_exceptions, WSMsgType
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.alexa import smart_home
|
||||
from homeassistant.components.alexa import smart_home as alexa
|
||||
from homeassistant.components.google_assistant import smart_home as ga
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from . import auth_api
|
||||
@@ -78,7 +79,7 @@ class CloudIoT:
|
||||
yield from hass.async_add_job(auth_api.check_token, self.cloud)
|
||||
|
||||
self.client = client = yield from session.ws_connect(
|
||||
self.cloud.relayer, headers={
|
||||
self.cloud.relayer, heartbeat=55, headers={
|
||||
hdrs.AUTHORIZATION:
|
||||
'Bearer {}'.format(self.cloud.id_token)
|
||||
})
|
||||
@@ -204,9 +205,18 @@ def async_handle_message(hass, cloud, handler_name, payload):
|
||||
@asyncio.coroutine
|
||||
def async_handle_alexa(hass, cloud, payload):
|
||||
"""Handle an incoming IoT message for Alexa."""
|
||||
return (yield from smart_home.async_handle_message(hass,
|
||||
cloud.alexa_config,
|
||||
payload))
|
||||
result = yield from alexa.async_handle_message(hass, cloud.alexa_config,
|
||||
payload)
|
||||
return result
|
||||
|
||||
|
||||
@HANDLERS.register('google_actions')
|
||||
@asyncio.coroutine
|
||||
def async_handle_google_actions(hass, cloud, payload):
|
||||
"""Handle an incoming IoT message for Google Actions."""
|
||||
result = yield from ga.async_handle_message(hass, cloud.gactions_config,
|
||||
payload)
|
||||
return result
|
||||
|
||||
|
||||
@HANDLERS.register('cloud')
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Support for Coinbase.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/coinbase/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['coinbase==2.0.6']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'coinbase'
|
||||
|
||||
CONF_API_SECRET = 'api_secret'
|
||||
CONF_EXCHANGE_CURRENCIES = 'exchange_rate_currencies'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||
|
||||
DATA_COINBASE = 'coinbase_cache'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_API_SECRET): cv.string,
|
||||
vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string])
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Coinbase component.
|
||||
|
||||
Will automatically setup sensors to support
|
||||
wallets discovered on the network.
|
||||
"""
|
||||
api_key = config[DOMAIN].get(CONF_API_KEY)
|
||||
api_secret = config[DOMAIN].get(CONF_API_SECRET)
|
||||
exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES)
|
||||
|
||||
hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key,
|
||||
api_secret)
|
||||
|
||||
if not hasattr(coinbase_data, 'accounts'):
|
||||
return False
|
||||
for account in coinbase_data.accounts.data:
|
||||
load_platform(hass, 'sensor', DOMAIN,
|
||||
{'account': account}, config)
|
||||
for currency in exchange_currencies:
|
||||
if currency not in coinbase_data.exchange_rates.rates:
|
||||
_LOGGER.warning("Currency %s not found", currency)
|
||||
continue
|
||||
native = coinbase_data.exchange_rates.currency
|
||||
load_platform(hass,
|
||||
'sensor',
|
||||
DOMAIN,
|
||||
{'native_currency': native,
|
||||
'exchange_currency': currency},
|
||||
config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CoinbaseData(object):
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, api_key, api_secret):
|
||||
"""Init the coinbase data object."""
|
||||
from coinbase.wallet.client import Client
|
||||
self.client = Client(api_key, api_secret)
|
||||
self.update()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from coinbase."""
|
||||
from coinbase.wallet.error import AuthenticationError
|
||||
try:
|
||||
self.accounts = self.client.get_accounts()
|
||||
self.exchange_rates = self.client.get_exchange_rates()
|
||||
except AuthenticationError as coinbase_error:
|
||||
_LOGGER.error("Authentication error connecting"
|
||||
" to coinbase: %s", coinbase_error)
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Provide configuration end points for Z-Wave."""
|
||||
"""Provide configuration end points for Automations."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.config import EditIdBasedConfigView
|
||||
|
||||
@@ -12,20 +12,27 @@ import warnings
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.components import http
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
from homeassistant.helpers import intent, config_validation as cv
|
||||
from homeassistant.components import http
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.16.0']
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.15.1']
|
||||
DEPENDENCIES = ['http']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
DOMAIN = 'conversation'
|
||||
|
||||
INTENT_TURN_OFF = 'HassTurnOff'
|
||||
INTENT_TURN_ON = 'HassTurnOn'
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
REGEX_TYPE = type(re.compile(''))
|
||||
|
||||
SERVICE_PROCESS = 'process'
|
||||
|
||||
@@ -39,12 +46,6 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
|
||||
})
|
||||
})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
INTENT_TURN_ON = 'HassTurnOn'
|
||||
INTENT_TURN_OFF = 'HassTurnOff'
|
||||
REGEX_TYPE = type(re.compile(''))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@core.callback
|
||||
@bind_hass
|
||||
|
||||
@@ -6,12 +6,10 @@ at https://home-assistant.io/components/counter/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -133,20 +131,12 @@ def async_setup(hass, config):
|
||||
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_INCREMENT, async_handler_service,
|
||||
descriptions[SERVICE_INCREMENT], SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_INCREMENT, async_handler_service)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DECREMENT, async_handler_service,
|
||||
descriptions[SERVICE_DECREMENT], SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_DECREMENT, async_handler_service)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESET, async_handler_service,
|
||||
descriptions[SERVICE_RESET], SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_RESET, async_handler_service)
|
||||
|
||||
yield from component.async_add_entities(entities)
|
||||
return True
|
||||
|
||||
@@ -8,11 +8,9 @@ import asyncio
|
||||
from datetime import timedelta
|
||||
import functools as ft
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -179,16 +177,12 @@ def async_setup(hass, config):
|
||||
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'))
|
||||
|
||||
for service_name in SERVICE_TO_METHOD:
|
||||
schema = SERVICE_TO_METHOD[service_name].get(
|
||||
'schema', COVER_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, service_name, async_handle_cover_service,
|
||||
descriptions.get(service_name), schema=schema)
|
||||
schema=schema)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ import logging
|
||||
from typing import Callable # noqa
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, DOMAIN
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
ISYDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING, STATE_UNKNOWN)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -17,44 +19,32 @@ _LOGGER = logging.getLogger(__name__)
|
||||
VALUE_TO_STATE = {
|
||||
0: STATE_CLOSED,
|
||||
101: STATE_UNKNOWN,
|
||||
102: 'stopped',
|
||||
103: STATE_CLOSING,
|
||||
104: STATE_OPENING
|
||||
}
|
||||
|
||||
UOM = ['97']
|
||||
STATES = [STATE_OPEN, STATE_CLOSED, 'closing', 'opening', 'stopped']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 cover platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.NODES, units=UOM, states=STATES):
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
devices.append(ISYCoverDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
actions = program[isy.KEY_ACTIONS]
|
||||
assert actions.dtype == 'program', 'Not a program'
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
devices.append(ISYCoverProgram(program.name, status, actions))
|
||||
for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]:
|
||||
devices.append(ISYCoverProgram(name, status, actions))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYCoverDevice(isy.ISYDevice, CoverDevice):
|
||||
class ISYCoverDevice(ISYDevice, CoverDevice):
|
||||
"""Representation of an ISY994 cover device."""
|
||||
|
||||
def __init__(self, node: object):
|
||||
"""Initialize the ISY994 cover device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int:
|
||||
@@ -69,7 +59,10 @@ class ISYCoverDevice(isy.ISYDevice, CoverDevice):
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 cover device."""
|
||||
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
|
||||
if self.is_unknown():
|
||||
return None
|
||||
else:
|
||||
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
|
||||
|
||||
def open_cover(self, **kwargs) -> None:
|
||||
"""Send the open cover command to the ISY994 cover device."""
|
||||
@@ -87,7 +80,7 @@ class ISYCoverProgram(ISYCoverDevice):
|
||||
|
||||
def __init__(self, name: str, node: object, actions: object) -> None:
|
||||
"""Initialize the ISY994 cover program."""
|
||||
ISYCoverDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
self._name = name
|
||||
self._actions = actions
|
||||
|
||||
|
||||
@@ -124,6 +124,11 @@ class KNXCover(CoverDevice):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.hass.data[DATA_KNX].connected
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed within KNX."""
|
||||
|
||||
@@ -21,8 +21,9 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
|
||||
STATE_CLOSED, STATE_UNKNOWN)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_AVAILABILITY_TOPIC,
|
||||
CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic)
|
||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN,
|
||||
valid_publish_topic, valid_subscribe_topic, MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -37,8 +38,6 @@ CONF_SET_POSITION_TEMPLATE = 'set_position_template'
|
||||
CONF_PAYLOAD_OPEN = 'payload_open'
|
||||
CONF_PAYLOAD_CLOSE = 'payload_close'
|
||||
CONF_PAYLOAD_STOP = 'payload_stop'
|
||||
CONF_PAYLOAD_AVAILABLE = 'payload_available'
|
||||
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
|
||||
CONF_STATE_OPEN = 'state_open'
|
||||
CONF_STATE_CLOSED = 'state_closed'
|
||||
CONF_TILT_CLOSED_POSITION = 'tilt_closed_value'
|
||||
@@ -52,8 +51,6 @@ DEFAULT_NAME = 'MQTT Cover'
|
||||
DEFAULT_PAYLOAD_OPEN = 'OPEN'
|
||||
DEFAULT_PAYLOAD_CLOSE = 'CLOSE'
|
||||
DEFAULT_PAYLOAD_STOP = 'STOP'
|
||||
DEFAULT_PAYLOAD_AVAILABLE = 'online'
|
||||
DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline'
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
DEFAULT_RETAIN = False
|
||||
DEFAULT_TILT_CLOSED_POSITION = 0
|
||||
@@ -73,16 +70,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_AVAILABILITY_TOPIC, default=None): valid_subscribe_topic,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_AVAILABLE): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
|
||||
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
|
||||
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
@@ -98,7 +90,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
default=DEFAULT_TILT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_TILT_INVERT_STATE,
|
||||
default=DEFAULT_TILT_INVERT_STATE): cv.boolean,
|
||||
})
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -143,7 +135,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)])
|
||||
|
||||
|
||||
class MqttCover(CoverDevice):
|
||||
class MqttCover(MqttAvailability, CoverDevice):
|
||||
"""Representation of a cover that can be controlled using MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, availability_topic,
|
||||
@@ -154,21 +146,19 @@ class MqttCover(CoverDevice):
|
||||
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
|
||||
tilt_invert, position_topic, set_position_template):
|
||||
"""Initialize the cover."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
self._position = None
|
||||
self._state = None
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._availability_topic = availability_topic
|
||||
self._available = True if availability_topic is None else False
|
||||
self._tilt_command_topic = tilt_command_topic
|
||||
self._tilt_status_topic = tilt_status_topic
|
||||
self._qos = qos
|
||||
self._payload_open = payload_open
|
||||
self._payload_close = payload_close
|
||||
self._payload_stop = payload_stop
|
||||
self._payload_available = payload_available
|
||||
self._payload_not_available = payload_not_available
|
||||
self._state_open = state_open
|
||||
self._state_closed = state_closed
|
||||
self._retain = retain
|
||||
@@ -186,10 +176,9 @@ class MqttCover(CoverDevice):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe MQTT events.
|
||||
"""Subscribe MQTT events."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def tilt_updated(topic, payload, qos):
|
||||
"""Handle tilt updates."""
|
||||
@@ -266,11 +255,6 @@ class MqttCover(CoverDevice):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if cover is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
|
||||
@@ -4,12 +4,29 @@ Support for RFXtrx cover components.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/cover.rfxtrx/
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.rfxtrx import (
|
||||
CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS,
|
||||
CONF_SIGNAL_REPETITIONS, CONF_DEVICES)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICES, default={}): {
|
||||
cv.string: vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean
|
||||
})
|
||||
},
|
||||
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS):
|
||||
vol.Coerce(int),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.tahoma/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.tahoma import (
|
||||
@@ -14,6 +15,8 @@ DEPENDENCIES = ['tahoma']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Tahoma covers."""
|
||||
@@ -70,4 +73,15 @@ class TahomaCover(TahomaDevice, CoverDevice):
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self.apply_action('stopIdentify')
|
||||
if self.tahoma_device.type == \
|
||||
'io:RollerShutterWithLowSpeedManagementIOComponent':
|
||||
self.apply_action('setPosition', 'secured')
|
||||
else:
|
||||
self.apply_action('stopIdentify')
|
||||
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
if self.tahoma_device.type == 'io:WindowOpenerVeluxIOComponent':
|
||||
return 'window'
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Support for Tellstick covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.tellstick/
|
||||
"""
|
||||
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.tellstick import (
|
||||
DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG,
|
||||
DATA_TELLSTICK, TellstickDevice)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tellstick covers."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
return
|
||||
|
||||
signal_repetitions = discovery_info.get(
|
||||
ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS)
|
||||
|
||||
add_devices([TellstickCover(hass.data[DATA_TELLSTICK][tellcore_id],
|
||||
signal_repetitions)
|
||||
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]],
|
||||
True)
|
||||
|
||||
|
||||
class TellstickCover(TellstickDevice, CoverDevice):
|
||||
"""Representation of a Tellstick cover."""
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of the cover is not possible."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return True if unable to access real state of the entity."""
|
||||
return True
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._tellcore_device.down()
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._tellcore_device.up()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._tellcore_device.stop()
|
||||
|
||||
def _parse_tellcore_data(self, tellcore_data):
|
||||
"""Turn the value received from tellcore into something useful."""
|
||||
pass
|
||||
|
||||
def _parse_ha_data(self, kwargs):
|
||||
"""Turn the value from HA into something useful."""
|
||||
pass
|
||||
|
||||
def _update_model(self, new_state, data):
|
||||
"""Update the device entity state to match the arguments."""
|
||||
pass
|
||||
@@ -63,10 +63,15 @@ COVER_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean,
|
||||
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_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
|
||||
})
|
||||
|
||||
COVER_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_ENTITY_ID),
|
||||
COVER_SCHEMA,
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
})
|
||||
|
||||
@@ -18,8 +18,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Vera covers."""
|
||||
add_devices(
|
||||
VeraCover(device, VERA_CONTROLLER) for
|
||||
device in VERA_DEVICES['cover'])
|
||||
VeraCover(device, hass.data[VERA_CONTROLLER]) for
|
||||
device in hass.data[VERA_DEVICES]['cover'])
|
||||
|
||||
|
||||
class VeraCover(VeraDevice, CoverDevice):
|
||||
|
||||
@@ -41,7 +41,7 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice):
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self.current_cover_position < 0
|
||||
return self.current_cover_position <= 0
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Platform for the Daikin AC.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
https://home-assistant.io/components/daikin/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from socket import timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.discovery import SERVICE_DAIKIN
|
||||
from homeassistant.const import (
|
||||
CONF_HOSTS, CONF_ICON, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TYPE
|
||||
)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pydaikin==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'daikin'
|
||||
HTTP_RESOURCES = ['aircon/get_sensor_info', 'aircon/get_control_info']
|
||||
|
||||
ATTR_TARGET_TEMPERATURE = 'target_temperature'
|
||||
ATTR_INSIDE_TEMPERATURE = 'inside_temperature'
|
||||
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
COMPONENT_TYPES = ['climate', 'sensor']
|
||||
|
||||
SENSOR_TYPE_TEMPERATURE = 'temperature'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
ATTR_INSIDE_TEMPERATURE: {
|
||||
CONF_NAME: 'Inside Temperature',
|
||||
CONF_ICON: 'mdi:thermometer',
|
||||
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
|
||||
},
|
||||
ATTR_OUTSIDE_TEMPERATURE: {
|
||||
CONF_NAME: 'Outside Temperature',
|
||||
CONF_ICON: 'mdi:thermometer',
|
||||
CONF_TYPE: SENSOR_TYPE_TEMPERATURE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_HOSTS, default=[]
|
||||
): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
default=list(SENSOR_TYPES.keys())
|
||||
): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)])
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Establish connection with Daikin."""
|
||||
def discovery_dispatch(service, discovery_info):
|
||||
"""Dispatcher for Daikin discovery events."""
|
||||
host = discovery_info.get('ip')
|
||||
|
||||
if daikin_api_setup(hass, host) is None:
|
||||
return
|
||||
|
||||
for component in COMPONENT_TYPES:
|
||||
load_platform(hass, component, DOMAIN, discovery_info,
|
||||
config)
|
||||
|
||||
discovery.listen(hass, SERVICE_DAIKIN, discovery_dispatch)
|
||||
|
||||
for host in config.get(DOMAIN, {}).get(CONF_HOSTS, []):
|
||||
if daikin_api_setup(hass, host) is None:
|
||||
continue
|
||||
|
||||
discovery_info = {
|
||||
'ip': host,
|
||||
CONF_MONITORED_CONDITIONS:
|
||||
config[DOMAIN][CONF_MONITORED_CONDITIONS]
|
||||
}
|
||||
load_platform(hass, 'sensor', DOMAIN, discovery_info, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def daikin_api_setup(hass, host, name=None):
|
||||
"""Create a Daikin instance only once."""
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
api = hass.data[DOMAIN].get(host)
|
||||
if api is None:
|
||||
from pydaikin import appliance
|
||||
|
||||
try:
|
||||
device = appliance.Appliance(host)
|
||||
except timeout:
|
||||
_LOGGER.error("Connection to Daikin could not be established")
|
||||
return False
|
||||
|
||||
if name is None:
|
||||
name = device.values['name']
|
||||
|
||||
api = DaikinApi(device, name)
|
||||
|
||||
return api
|
||||
|
||||
|
||||
class DaikinApi(object):
|
||||
"""Keep the Daikin instance in one place and centralize the update."""
|
||||
|
||||
def __init__(self, device, name):
|
||||
"""Initialize the Daikin Handle."""
|
||||
self.device = device
|
||||
self.name = name
|
||||
self.ip_address = device.ip
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self, **kwargs):
|
||||
"""Pull the latest data from Daikin."""
|
||||
try:
|
||||
for resource in HTTP_RESOURCES:
|
||||
self.device.values.update(
|
||||
self.device.get_resource(resource)
|
||||
)
|
||||
except timeout:
|
||||
_LOGGER.warning(
|
||||
"Connection failed for %s", self.ip_address
|
||||
)
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Support for deCONZ devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/deconz/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.discovery import SERVICE_DECONZ
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['pydeconz==23']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'deconz'
|
||||
|
||||
CONFIG_FILE = 'deconz.conf'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_PORT, default=80): cv.port,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_FIELD = 'field'
|
||||
SERVICE_DATA = 'data'
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(SERVICE_FIELD): cv.string,
|
||||
vol.Required(SERVICE_DATA): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_INSTRUCTIONS = """
|
||||
Unlock your deCONZ gateway to register with Home Assistant.
|
||||
|
||||
1. [Go to deCONZ system settings](http://{}:{}/edit_system.html)
|
||||
2. Press "Unlock Gateway" button
|
||||
|
||||
[deCONZ platform documentation](https://home-assistant.io/components/deconz/)
|
||||
"""
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup services and configuration for deCONZ component."""
|
||||
result = False
|
||||
config_file = yield from hass.async_add_job(
|
||||
load_json, hass.config.path(CONFIG_FILE))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_deconz_discovered(service, discovery_info):
|
||||
"""Called when deCONZ gateway has been found."""
|
||||
deconz_config = {}
|
||||
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
|
||||
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
|
||||
yield from async_request_configuration(hass, config, deconz_config)
|
||||
|
||||
if config_file:
|
||||
result = yield from async_setup_deconz(hass, config, config_file)
|
||||
|
||||
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
|
||||
deconz_config = config[DOMAIN]
|
||||
if CONF_API_KEY in deconz_config:
|
||||
result = yield from async_setup_deconz(hass, config, deconz_config)
|
||||
else:
|
||||
yield from async_request_configuration(hass, config, deconz_config)
|
||||
return True
|
||||
|
||||
if not result:
|
||||
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_deconz(hass, config, deconz_config):
|
||||
"""Setup deCONZ session.
|
||||
|
||||
Load config, group, light and sensor data for server information.
|
||||
Start websocket for push notification of state changes from deCONZ.
|
||||
"""
|
||||
from pydeconz import DeconzSession
|
||||
websession = async_get_clientsession(hass)
|
||||
deconz = DeconzSession(hass.loop, websession, **deconz_config)
|
||||
result = yield from deconz.async_load_parameters()
|
||||
if result is False:
|
||||
_LOGGER.error("Failed to communicate with deCONZ.")
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN] = deconz
|
||||
|
||||
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, component, DOMAIN, {}, config))
|
||||
deconz.start()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_configure(call):
|
||||
"""Set attribute of device in deCONZ.
|
||||
|
||||
Field is a string representing a specific device in deCONZ
|
||||
e.g. field='/lights/1/state'.
|
||||
Data is a json object with what data you want to alter
|
||||
e.g. data={'on': true}.
|
||||
{
|
||||
"field": "/lights/1/state",
|
||||
"data": {"on": true}
|
||||
}
|
||||
See Dresden Elektroniks REST API documentation for details:
|
||||
http://dresden-elektronik.github.io/deconz-rest-doc/rest/
|
||||
"""
|
||||
deconz = hass.data[DOMAIN]
|
||||
field = call.data.get(SERVICE_FIELD)
|
||||
data = call.data.get(SERVICE_DATA)
|
||||
yield from deconz.async_put_state(field, data)
|
||||
hass.services.async_register(
|
||||
DOMAIN, 'configure', async_configure,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz.close)
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_request_configuration(hass, config, deconz_config):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_configuration_callback(data):
|
||||
"""Set up actions to do when our configuration callback is called."""
|
||||
from pydeconz.utils import async_get_api_key
|
||||
api_key = yield from async_get_api_key(hass.loop, **deconz_config)
|
||||
if api_key:
|
||||
deconz_config[CONF_API_KEY] = api_key
|
||||
result = yield from async_setup_deconz(hass, config, deconz_config)
|
||||
if result:
|
||||
yield from hass.async_add_job(save_json,
|
||||
hass.config.path(CONFIG_FILE),
|
||||
deconz_config)
|
||||
configurator.async_request_done(request_id)
|
||||
return
|
||||
else:
|
||||
configurator.async_notify_errors(
|
||||
request_id, "Couldn't load configuration.")
|
||||
else:
|
||||
configurator.async_notify_errors(
|
||||
request_id, "Couldn't get an API key.")
|
||||
return
|
||||
|
||||
instructions = CONFIG_INSTRUCTIONS.format(
|
||||
deconz_config[CONF_HOST], deconz_config[CONF_PORT])
|
||||
|
||||
request_id = configurator.async_request_config(
|
||||
"deCONZ", async_configuration_callback,
|
||||
description=instructions,
|
||||
entity_picture="/static/images/logo_deconz.jpeg",
|
||||
submit_caption="I have unlocked the gateway",
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
configure:
|
||||
description: Set attribute of device in Deconz. See Dresden Elektroniks REST API documentation for details http://dresden-elektronik.github.io/deconz-rest-doc/rest/
|
||||
fields:
|
||||
field:
|
||||
description: Field is a string representing a specific device in Deconz.
|
||||
example: '/lights/1/state'
|
||||
data:
|
||||
description: Data is a json object with what data you want to alter.
|
||||
example: '{"on": true}'
|
||||
@@ -7,7 +7,6 @@ https://home-assistant.io/components/device_tracker/
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, List, Sequence, Callable
|
||||
|
||||
import aiohttp
|
||||
@@ -53,6 +52,7 @@ YAML_DEVICES = 'known_devices.yaml'
|
||||
|
||||
CONF_TRACK_NEW = 'track_new_devices'
|
||||
DEFAULT_TRACK_NEW = True
|
||||
CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults'
|
||||
|
||||
CONF_CONSIDER_HOME = 'consider_home'
|
||||
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
|
||||
@@ -80,13 +80,21 @@ ATTR_VENDOR = 'vendor'
|
||||
|
||||
SOURCE_TYPE_GPS = 'gps'
|
||||
SOURCE_TYPE_ROUTER = 'router'
|
||||
SOURCE_TYPE_BLUETOOTH = 'bluetooth'
|
||||
SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le'
|
||||
|
||||
NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
|
||||
}))
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_CONSIDER_HOME,
|
||||
default=DEFAULT_CONSIDER_HOME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_NEW_DEVICE_DEFAULTS,
|
||||
default={}): NEW_DEVICE_DEFAULTS_SCHEMA
|
||||
})
|
||||
|
||||
|
||||
@@ -124,10 +132,15 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
conf = config.get(DOMAIN, [])
|
||||
conf = conf[0] if conf else {}
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
|
||||
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
|
||||
track_new = conf.get(CONF_TRACK_NEW)
|
||||
if track_new is None:
|
||||
track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
|
||||
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
||||
tracker = DeviceTracker(
|
||||
hass, consider_home, track_new, defaults, devices)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(p_type, p_config, disc_info=None):
|
||||
@@ -195,12 +208,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
|
||||
yield from tracker.async_see(**args)
|
||||
|
||||
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_SEE, async_see_service, descriptions.get(SERVICE_SEE))
|
||||
hass.services.async_register(DOMAIN, SERVICE_SEE, async_see_service)
|
||||
|
||||
# restore
|
||||
yield from tracker.async_setup_tracked_device()
|
||||
@@ -211,13 +219,16 @@ class DeviceTracker(object):
|
||||
"""Representation of a device tracker."""
|
||||
|
||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track_new: bool, devices: Sequence) -> None:
|
||||
track_new: bool, defaults: dict,
|
||||
devices: Sequence) -> None:
|
||||
"""Initialize a device tracker."""
|
||||
self.hass = hass
|
||||
self.devices = {dev.dev_id: dev for dev in devices}
|
||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new
|
||||
self.track_new = track_new if track_new is not None \
|
||||
else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
self.defaults = defaults
|
||||
self.group = None
|
||||
self._is_updating = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
@@ -274,7 +285,8 @@ class DeviceTracker(object):
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '),
|
||||
picture=picture, icon=icon)
|
||||
picture=picture, icon=icon,
|
||||
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user