Compare commits
584 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f046972d9 | |||
| 53f8828181 | |||
| b6b40286ef | |||
| 8a86ec5b74 | |||
| 20c5f9de4b | |||
| 61730012d8 | |||
| 25d2df5689 | |||
| 672b83db8a | |||
| b37438ebb7 | |||
| 902b72ba1a | |||
| f10fede17f | |||
| c9548b11b1 | |||
| f4aec3ac88 | |||
| e7425e9808 | |||
| 978b539111 | |||
| 9f4cd5fafe | |||
| ba3c9f9765 | |||
| 4ee8be52fe | |||
| 866bf887d3 | |||
| dddbce82f5 | |||
| be15ca3f23 | |||
| 9a305c9742 | |||
| de231cf9ab | |||
| 8325f9db8a | |||
| 3f38b9e52f | |||
| 1cb2a6add0 | |||
| acf75b5253 | |||
| 970bde9e99 | |||
| 796143a6c6 | |||
| 5569ae38f1 | |||
| 7eaad4fb3a | |||
| 35c679a956 | |||
| 678f273002 | |||
| 4e91c65d6e | |||
| f6106706e5 | |||
| ecf337b123 | |||
| f5d8327d9a | |||
| 30d4c54187 | |||
| edf20f542a | |||
| 9778000e9a | |||
| b5149dfba6 | |||
| ced3cd2616 | |||
| 7050236a61 | |||
| c8e1ffad89 | |||
| e3edff8a72 | |||
| d6fd0f405e | |||
| 1ab47b5d2b | |||
| a2365eccf6 | |||
| c46ba3446d | |||
| 5714f156c3 | |||
| 959dd29c90 | |||
| e75a66ed20 | |||
| 3e72aa8643 | |||
| 5aaa1f8404 | |||
| 7292e564f8 | |||
| 509cfb6433 | |||
| acdab67c1b | |||
| d7addf59cd | |||
| ccf9edf815 | |||
| 2fd3c186e2 | |||
| 719199da45 | |||
| a3cd7d653d | |||
| aeeb927e19 | |||
| a3a14f9ea4 | |||
| f4e7b231bc | |||
| 5f68735375 | |||
| 774fd19638 | |||
| e265401cd0 | |||
| 5b3dc7f2a5 | |||
| 9ef084d903 | |||
| 95b1e257bb | |||
| b06cf87c74 | |||
| 326337777a | |||
| 2c8a06bfbe | |||
| 198a234468 | |||
| e94aa3afe9 | |||
| 96e22c7b41 | |||
| 33450c726d | |||
| 97b9d3bd21 | |||
| fff589eeab | |||
| 5e5d2e8ab8 | |||
| 1a7ffdca52 | |||
| cada74df22 | |||
| bd3fbe8363 | |||
| 4d9c7d9684 | |||
| 0bf66384ed | |||
| c7798ef43c | |||
| f4d8095e54 | |||
| 5529d77c62 | |||
| c4e151f621 | |||
| f58941a0d4 | |||
| bca673f039 | |||
| 2687f2f623 | |||
| 134b3d2f3b | |||
| f450c1351c | |||
| c6b10f3703 | |||
| 4a08067b9c | |||
| 9c37437a59 | |||
| 9330142987 | |||
| 2bbaac44d4 | |||
| 253dee8e4d | |||
| 5722cf53bf | |||
| 5d301590c3 | |||
| 353f5d6b49 | |||
| 11da7bed12 | |||
| a358c8e10d | |||
| 58826b264a | |||
| 56abc7f9b4 | |||
| 55d60a6a13 | |||
| 5183cb5903 | |||
| 0aa8933df6 | |||
| fc46a24996 | |||
| 5be58bd056 | |||
| 4a423e63f3 | |||
| a8b32edc8e | |||
| 4a5b9db394 | |||
| 9c23178457 | |||
| 764d31efcb | |||
| fc90ccea36 | |||
| aab63ea22a | |||
| 157ab77232 | |||
| 9a86ccaaea | |||
| 32dd815852 | |||
| 9ac3928600 | |||
| 62e57456e1 | |||
| 11f11481b2 | |||
| 10f5e9744b | |||
| b2a2193ba3 | |||
| c8b2ba6559 | |||
| 1496f1a570 | |||
| 493c0bbb4c | |||
| 13dd17b2ab | |||
| 0ef1c3af34 | |||
| 44da43065f | |||
| ffb1613d55 | |||
| b0a2909835 | |||
| b5fb558c62 | |||
| 846a0513c7 | |||
| 0f1cad24ce | |||
| 24630b1ebe | |||
| b705b3ddb9 | |||
| 330d352d3a | |||
| bdaae6f844 | |||
| 49308bec13 | |||
| edd96c2b04 | |||
| 11c4d3892f | |||
| 75817ad46d | |||
| 780cdd5f90 | |||
| d70eaeb118 | |||
| 9e9c6d0184 | |||
| 2cdc0febf5 | |||
| c721ec7ab0 | |||
| 4e63e8328e | |||
| 8f2009a187 | |||
| 529eee994b | |||
| 3f7dd7ed9a | |||
| b952cfe705 | |||
| b9cf4df557 | |||
| 7892297240 | |||
| 20fcd1f0e2 | |||
| 8bbf13ef9f | |||
| 855756cb2a | |||
| bbcfb9158a | |||
| bcd4def0ae | |||
| ddc260b628 | |||
| 06bc062221 | |||
| 21f3b62d09 | |||
| 185ccc4fc4 | |||
| 2d337fd29a | |||
| c625e219c7 | |||
| b1736994b7 | |||
| 21feff5fd8 | |||
| 93118fcade | |||
| 5e1c74b430 | |||
| 3d3a0a7a4f | |||
| fdd1957750 | |||
| 2b97449d98 | |||
| c937a7bcb0 | |||
| e7f442d66b | |||
| 2c5d3387f2 | |||
| bb4f23f8e7 | |||
| 629b2e81ba | |||
| 3508f74fb2 | |||
| d16bc632da | |||
| 44d4987536 | |||
| 470702261a | |||
| 5fb7aa212b | |||
| 9522fe3a92 | |||
| ff3c90fb80 | |||
| 90ad54da7d | |||
| 2baa838ba7 | |||
| a8add06a40 | |||
| 1b23b32817 | |||
| eaaa0442e2 | |||
| bc9f2d21c4 | |||
| 1a139234af | |||
| d5435cf066 | |||
| 46ec6d6dce | |||
| 660e777f01 | |||
| de038bae65 | |||
| 7774f0ae53 | |||
| 7655b6271d | |||
| 10bf659773 | |||
| e8a22cb4a8 | |||
| 2650c73a89 | |||
| bdf948d866 | |||
| 928e025910 | |||
| 96aae1292b | |||
| c0bf3d7f32 | |||
| 307514e3a7 | |||
| b939626497 | |||
| 1522e67351 | |||
| 8232f1ef65 | |||
| 78f5a8a6f8 | |||
| 3044aecbe9 | |||
| a5081ac307 | |||
| f396a4593e | |||
| aac9f972cf | |||
| aaa0944595 | |||
| 6cca127bbf | |||
| c1f3ce78e1 | |||
| 887b53b794 | |||
| a444df3fde | |||
| b038a1650e | |||
| 483556ac5b | |||
| 3e9e388745 | |||
| ab42acf4d7 | |||
| 0489ae53c4 | |||
| 35fcc299c0 | |||
| 568c549353 | |||
| edf130b341 | |||
| ed9e93c29f | |||
| 55f8ec8866 | |||
| 3e70154695 | |||
| aa17481c94 | |||
| b53bc24a63 | |||
| fbd0bf77c7 | |||
| 4da2156ebf | |||
| 8a67fcfee3 | |||
| 08f9793175 | |||
| a5b2fc9759 | |||
| 3fa8aff78e | |||
| 09ff9cb08e | |||
| c32300a386 | |||
| 55dc483c91 | |||
| 597ae2e716 | |||
| 50887e7e2c | |||
| 8743f23f13 | |||
| 72fe50bef6 | |||
| bae6333c26 | |||
| a08539d88d | |||
| bf7aecce90 | |||
| e2aa024a05 | |||
| edd5db296d | |||
| e14d6f11c6 | |||
| f3870a8a48 | |||
| 31bf5b8ff0 | |||
| a7ae456a06 | |||
| c03022efa3 | |||
| 46f5a65e68 | |||
| 44ec6b056e | |||
| 354007f265 | |||
| 6cb8a36cf1 | |||
| 435f253be8 | |||
| 0fe41ffb00 | |||
| bafa0cc3b8 | |||
| e23aa1ccf8 | |||
| a9db6b16eb | |||
| bf30f2e9e8 | |||
| ee9d50c0a5 | |||
| 6736534a52 | |||
| 4ccd819ec5 | |||
| 52b1e13aca | |||
| 67f3910f03 | |||
| 64cb3390ea | |||
| 0ac4a152be | |||
| 4e96e461f7 | |||
| 30bed8341a | |||
| d17733427b | |||
| 782d2a30cd | |||
| 84f30d9ef8 | |||
| ac49298c8d | |||
| a0256e1947 | |||
| 7bc2e1238d | |||
| 41f558b181 | |||
| 383b0914b3 | |||
| be297c4c7e | |||
| aa1f64bed6 | |||
| faf8bbcf13 | |||
| 0fa259089d | |||
| f7c7073cd7 | |||
| d7bf3920a5 | |||
| 7ee75d67c5 | |||
| d7db3aba36 | |||
| 7f99e99dad | |||
| fbea5b4cac | |||
| e6c88c05ad | |||
| 757813411d | |||
| 7dc05785cc | |||
| d7af43b87d | |||
| 6ea74ce740 | |||
| d5bdf7783e | |||
| 65d255a626 | |||
| 53a735a329 | |||
| 68713822fd | |||
| 3d26ac3323 | |||
| d487960ad8 | |||
| 9403cdd2a5 | |||
| 1fb1a32c9a | |||
| 31ddcc6278 | |||
| d789de9ea2 | |||
| e2014eb153 | |||
| 5932446508 | |||
| 61909e873f | |||
| 48cf7a4af9 | |||
| 9490cb4c8f | |||
| 86d4d10176 | |||
| 7b3b755aaf | |||
| f5a1cd5b3c | |||
| 7b9f7889d2 | |||
| 9afcbaed1d | |||
| 58f9455604 | |||
| 596f7b99f6 | |||
| 94183e1992 | |||
| 0e5df9b641 | |||
| 3a7cc9bb45 | |||
| ecd1da6525 | |||
| a19a285bb0 | |||
| eb36174b51 | |||
| 6e51f7d987 | |||
| 4ba9020859 | |||
| 505725f9d3 | |||
| 2566d01aaa | |||
| 477f621705 | |||
| 7fcc3ae00a | |||
| 9f2719bb1f | |||
| 85d0f2e861 | |||
| a44d849405 | |||
| 5d007e636b | |||
| 7cd6f9038c | |||
| a80fd2f243 | |||
| 2487d27c45 | |||
| be7162a0df | |||
| c5a8372f13 | |||
| 81ca978413 | |||
| d6818c7015 | |||
| df5866dd95 | |||
| 34ee2b1ae9 | |||
| 8ca897da57 | |||
| c7fcd98cad | |||
| 8aa3124aa6 | |||
| b27ba9660b | |||
| 9f04b55572 | |||
| c4f4a9a158 | |||
| e2e8b43902 | |||
| 3a35642dc1 | |||
| 34a7aa2376 | |||
| 58eb32bce4 | |||
| c940d26f07 | |||
| fc5e25a07b | |||
| 1d32bced1c | |||
| f2a2d6bfa1 | |||
| 4f990ce488 | |||
| 106b7a9d8f | |||
| 1cd1facbd0 | |||
| b725eaf67f | |||
| 89807f24ad | |||
| 7935c54420 | |||
| 46d0d38444 | |||
| 5da110d764 | |||
| c88527ce79 | |||
| 6127173d2a | |||
| 1c6ba989a9 | |||
| 827e3c4395 | |||
| eefedaf332 | |||
| ac1e811dcd | |||
| 49b4cd3c41 | |||
| ef87d4dad4 | |||
| 960af20cc9 | |||
| 6c91e04852 | |||
| 74837dbf45 | |||
| 3b693d5e70 | |||
| 23dd76cdc5 | |||
| 896e0476ff | |||
| b0d3bbed79 | |||
| 65ed85c6eb | |||
| aee8758fc1 | |||
| 8983b826c4 | |||
| 11d3093a30 | |||
| 15e8a22100 | |||
| 10fb30e924 | |||
| b7b1429ac7 | |||
| 9f4a9585d2 | |||
| c1be5ede1c | |||
| 3beb87c54d | |||
| fdc373f27e | |||
| 2b9fb73032 | |||
| 36cda8c6b2 | |||
| 041f1edd35 | |||
| 32873508b7 | |||
| addd955a6b | |||
| 022afcf050 | |||
| 7ce2b9e018 | |||
| f256d1fe2f | |||
| 1910440a3c | |||
| 1d4c3febee | |||
| 1d7ab0fa95 | |||
| 14cf5b884b | |||
| 3d34368e6e | |||
| e425801fe0 | |||
| 73a4c09597 | |||
| 1a4b62909b | |||
| 37a8035c54 | |||
| 25408941de | |||
| b969fea900 | |||
| 2cc6fe6609 | |||
| 66d8787d47 | |||
| 5d8e219448 | |||
| 58f813b518 | |||
| dee4c85c32 | |||
| a4318c3125 | |||
| 5f095b5126 | |||
| 3cb1a5dd89 | |||
| dfbef45e49 | |||
| 9e73115337 | |||
| beb8b4b11f | |||
| 003815c91a | |||
| e1cbd6b4c0 | |||
| fa2c1dafdf | |||
| 76d1ee9fc2 | |||
| f29ee24b72 | |||
| b277fd55f9 | |||
| 75df4be733 | |||
| 86a1b0a6c6 | |||
| 799fbe42f8 | |||
| c1eed148cc | |||
| 62fe9f955e | |||
| b857f838df | |||
| c2dc940819 | |||
| ca9eb31d1d | |||
| 8a5fe38d69 | |||
| 91c3a49a5b | |||
| 1f72506f9b | |||
| 843840b963 | |||
| bb64560089 | |||
| ba305ee71c | |||
| 905f4bf994 | |||
| a496a7c792 | |||
| 714ba31b75 | |||
| 2574b915dd | |||
| 1eceb405ce | |||
| 8bef7d84bb | |||
| 75e41a21c9 | |||
| 9c176ad85a | |||
| 0f1a254f3b | |||
| 6674a8ad57 | |||
| 235d0057b1 | |||
| eb9400de4c | |||
| 4addcccfac | |||
| f6e46aecf5 | |||
| e9cf5f6f42 | |||
| e221c8a37d | |||
| b163544e3c | |||
| a718e92708 | |||
| 44d274e428 | |||
| c404fb7142 | |||
| 5895f431b4 | |||
| 0d06454a94 | |||
| fdb6dd81ce | |||
| 2d33ee6258 | |||
| b1fa178df4 | |||
| cf99551110 | |||
| 58e707a264 | |||
| c1988acb36 | |||
| 7776bfefc2 | |||
| ad95b2715e | |||
| d6f525a23f | |||
| b1eb3243bd | |||
| 9bcc692ff2 | |||
| 91d2ba609e | |||
| bbc5c3a300 | |||
| 04f3fe0ba3 | |||
| ea26aa2c81 | |||
| 71dc41655c | |||
| 29c7987453 | |||
| 80bc2666ac | |||
| 039559882b | |||
| 2993a4a7a5 | |||
| 30ad8bcc80 | |||
| 67d35e6454 | |||
| 409b74b780 | |||
| 4b8e6e36b6 | |||
| cd9f3fa215 | |||
| a06f89085d | |||
| 1bdd8e235a | |||
| 39ca1a5a0d | |||
| e17410c9a1 | |||
| f82ac0af60 | |||
| 462b47c725 | |||
| 52567b1a48 | |||
| ffb46ab541 | |||
| 0effe14619 | |||
| e70b7ab509 | |||
| 36c196f9e8 | |||
| 6005933451 | |||
| 41c2392f8b | |||
| e866eeb518 | |||
| 32fc164df3 | |||
| 5cad539859 | |||
| 401263519d | |||
| 0feb1c3e28 | |||
| b2d1774293 | |||
| a8dc559519 | |||
| 6d7041cd42 | |||
| 2ffdf1fdcd | |||
| f77eda2981 | |||
| b6404d70ec | |||
| 9a5618fe96 | |||
| bc1d14f9c3 | |||
| 18f38229b2 | |||
| feb2ebbc03 | |||
| 7b56fe2af6 | |||
| 8ca3ca8564 | |||
| 7401ec96b5 | |||
| 41849eab06 | |||
| 4623d1071e | |||
| 6311f21d31 | |||
| f4372a7df5 | |||
| 9b0a3e4c5a | |||
| 8d0731e9fc | |||
| 3a262cd7e0 | |||
| 9aac2113b6 | |||
| 5f0b2a7d15 | |||
| 64cb65a04e | |||
| 3f675afd5b | |||
| 289767522b | |||
| 675dd04e97 | |||
| e5085bf620 | |||
| 780173befb | |||
| dab6d011ca | |||
| dc6a28a8b2 | |||
| b2fae212cb | |||
| 9c400de64b | |||
| e4bbe37112 | |||
| 2103bfc824 | |||
| f8be731891 | |||
| 4af9d0f9ea | |||
| 533d28ce40 | |||
| a25e394a11 | |||
| 44311193ef | |||
| 2b2c1562a5 | |||
| abaf9e53c2 | |||
| 5ad934907a | |||
| 88653e66c8 | |||
| b981bfba7e | |||
| 2711c12928 | |||
| 0aad6c72d2 | |||
| d32949b099 | |||
| f5c58748b7 | |||
| 3a7309ab62 | |||
| 75887e6069 | |||
| e877d572f5 | |||
| 67957cbfa8 | |||
| b10d20bcab | |||
| cbf3a2ecae | |||
| 23ff2eb79c | |||
| 6ffab53377 | |||
| c7c3b30e0a | |||
| 0b5191a247 | |||
| 4e8d20328a | |||
| 8785e5826e | |||
| 1da6181491 | |||
| a150a69cca | |||
| 5bd54f69cc | |||
| 0d76d72b9f | |||
| 5ecef6aaac | |||
| be08bf0ef7 | |||
| 4c5e6399e9 | |||
| 2e8e5a35b5 | |||
| 841321f154 | |||
| ebfff6a907 | |||
| ecbbb06b2f | |||
| c54517de90 | |||
| f3b9fa2f41 |
+53
-9
@@ -14,9 +14,15 @@ omit =
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
homeassistant/components/android_ip_webcam.py
|
||||
homeassistant/components/*/android_ip_webcam.py
|
||||
|
||||
homeassistant/components/bbb_gpio.py
|
||||
homeassistant/components/*/bbb_gpio.py
|
||||
|
||||
homeassistant/components/blink.py
|
||||
homeassistant/components/*/blink.py
|
||||
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
@@ -41,6 +47,9 @@ omit =
|
||||
homeassistant/components/insteon_local.py
|
||||
homeassistant/components/*/insteon_local.py
|
||||
|
||||
homeassistant/components/insteon_plm.py
|
||||
homeassistant/components/*/insteon_plm.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
homeassistant/components/*/ios.py
|
||||
|
||||
@@ -50,6 +59,9 @@ omit =
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
homeassistant/components/lutron_caseta.py
|
||||
homeassistant/components/*/lutron_caseta.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
@@ -82,12 +94,19 @@ omit =
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/twilio.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
homeassistant/components/vera.py
|
||||
homeassistant/components/*/vera.py
|
||||
|
||||
homeassistant/components/verisure.py
|
||||
homeassistant/components/*/verisure.py
|
||||
|
||||
homeassistant/components/volvooncall.py
|
||||
homeassistant/components/*/volvooncall.py
|
||||
|
||||
homeassistant/components/*/webostv.py
|
||||
|
||||
homeassistant/components/wemo.py
|
||||
@@ -99,9 +118,6 @@ omit =
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zwave/*
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
@@ -126,6 +142,12 @@ omit =
|
||||
homeassistant/components/zabbix.py
|
||||
homeassistant/components/*/zabbix.py
|
||||
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
@@ -149,10 +171,13 @@ omit =
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/config/zwave.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/myq.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/cover/wink.py
|
||||
@@ -181,9 +206,7 @@ omit =
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/device_tracker/volvooncall.py
|
||||
homeassistant/components/device_tracker/xiaomi.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
@@ -207,6 +230,7 @@ omit =
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
@@ -216,6 +240,7 @@ omit =
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/clementine.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/denonavr.py
|
||||
@@ -223,7 +248,9 @@ omit =
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/frontier_silicon.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/hdmi_cec.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
@@ -233,6 +260,7 @@ omit =
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/nad.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/openhome.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pandora.py
|
||||
homeassistant/components/media_player/philips_js.py
|
||||
@@ -245,10 +273,12 @@ omit =
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
@@ -267,6 +297,7 @@ omit =
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
homeassistant/components/notify/pushetta.py
|
||||
homeassistant/components/notify/pushover.py
|
||||
homeassistant/components/notify/pushsafer.py
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/sendgrid.py
|
||||
homeassistant/components/notify/simplepush.py
|
||||
@@ -275,12 +306,11 @@ omit =
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/telstra.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/amcrest.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
@@ -291,21 +321,26 @@ omit =
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.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/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
@@ -317,11 +352,13 @@ omit =
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/mhz19.py
|
||||
homeassistant/components/sensor/lyft.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
@@ -334,6 +371,7 @@ omit =
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
@@ -358,6 +396,7 @@ omit =
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/usps.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
@@ -386,13 +425,18 @@ omit =
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/telegram_webhooks.py
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/__init__.py
|
||||
homeassistant/components/zwave/util.py
|
||||
homeassistant/components/zwave/workaround.py
|
||||
|
||||
|
||||
[report]
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
**Description:**
|
||||
## Description:
|
||||
|
||||
|
||||
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
|
||||
|
||||
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
|
||||
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
## Example entry for `configuration.yaml` (if applicable):
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
## Checklist:
|
||||
|
||||
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)
|
||||
@@ -26,5 +26,5 @@ If the code does not interact with devices:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L16
|
||||
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L51
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14
|
||||
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L54
|
||||
|
||||
@@ -14,6 +14,8 @@ matrix:
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.6-dev"
|
||||
env: TOXENV=py36
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
include README.rst
|
||||
include LICENSE
|
||||
include LICENSE.md
|
||||
graft homeassistant
|
||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
+2
-1
@@ -75,7 +75,8 @@ Build home automation on top of your devices:
|
||||
`Instapush <https://instapush.im>`__, `Notify My Android
|
||||
(NMA) <http://www.notifymyandroid.com/>`__,
|
||||
`PushBullet <https://www.pushbullet.com/>`__,
|
||||
`PushOver <https://pushover.net/>`__, `Slack <https://slack.com/>`__,
|
||||
`PushOver <https://pushover.net/>`__,
|
||||
`Slack <https://slack.com/>`__,
|
||||
`Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber
|
||||
(XMPP) <http://xmpp.org>`__
|
||||
|
||||
|
||||
@@ -255,10 +255,13 @@ 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('/__main__.py'):
|
||||
if sys.argv[0].endswith(os.path.sep + '__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 arg != '--daemon']
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
arg != '--daemon']
|
||||
else:
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
|
||||
+49
-394
@@ -4,315 +4,30 @@ import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from time import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.util.async import (
|
||||
run_coroutine_threadsafe, run_callback_threadsafe)
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
_PERSISTENT_ERRORS = {}
|
||||
HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)'
|
||||
|
||||
|
||||
def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_setup_component(hass, domain, config), loop=hass.loop).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if domain in hass.config.components:
|
||||
_LOGGER.debug('Component %s already set up.', domain)
|
||||
return True
|
||||
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
if config is None:
|
||||
config = {}
|
||||
|
||||
components = loader.load_order_component(domain)
|
||||
|
||||
# OrderedSet is empty if component or dependencies could not be resolved
|
||||
if not components:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
for component in components:
|
||||
res = yield from _async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error('Component %s failed to setup', component)
|
||||
_async_persistent_notification(hass, component, True)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_requirements(hass: core.HomeAssistant, component,
|
||||
name: str) -> bool:
|
||||
"""Install the requirements for a component.
|
||||
|
||||
This method needs to run in an executor.
|
||||
"""
|
||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||
return True
|
||||
|
||||
for req in component.REQUIREMENTS:
|
||||
if not pkg_util.install_package(req, target=hass.config.path('deps')):
|
||||
_LOGGER.error('Not initializing %s because could not install '
|
||||
'dependency %s', name, req)
|
||||
_async_persistent_notification(hass, name)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_setup_component(hass: core.HomeAssistant,
|
||||
domain: str, config) -> bool:
|
||||
"""Setup a component for Home Assistant.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
setup_progress = hass.data.get('setup_progress')
|
||||
if setup_progress is None:
|
||||
setup_progress = hass.data['setup_progress'] = []
|
||||
|
||||
if domain in setup_progress:
|
||||
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
||||
domain, domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Used to indicate to discovery that a setup is ongoing and allow it
|
||||
# to wait till it is done.
|
||||
did_lock = False
|
||||
if not setup_lock.locked():
|
||||
yield from setup_lock.acquire()
|
||||
did_lock = True
|
||||
|
||||
setup_progress.append(domain)
|
||||
config = yield from async_prepare_setup_component(hass, config, domain)
|
||||
|
||||
if config is None:
|
||||
return False
|
||||
|
||||
component = loader.get_component(domain)
|
||||
if component is None:
|
||||
_async_persistent_notification(hass, domain)
|
||||
return False
|
||||
|
||||
async_comp = hasattr(component, 'async_setup')
|
||||
|
||||
try:
|
||||
_LOGGER.info("Setting up %s", domain)
|
||||
if async_comp:
|
||||
result = yield from component.async_setup(hass, config)
|
||||
else:
|
||||
result = yield from hass.loop.run_in_executor(
|
||||
None, component.setup, hass, config)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error during setup of component %s', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
if result is False:
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
elif result is not True:
|
||||
_LOGGER.error('component %s did not return boolean if setup '
|
||||
'was successful. Disabling component.', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
loader.set_component(domain, None)
|
||||
return False
|
||||
|
||||
hass.config.components.append(component.DOMAIN)
|
||||
|
||||
hass.bus.async_fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||
)
|
||||
|
||||
return True
|
||||
finally:
|
||||
setup_progress.remove(domain)
|
||||
if did_lock:
|
||||
setup_lock.release()
|
||||
|
||||
|
||||
def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_component(hass, config, domain), loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
component = loader.get_component(domain)
|
||||
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
||||
if dep not in hass.config.components]
|
||||
|
||||
if missing_deps:
|
||||
_LOGGER.error(
|
||||
'Not initializing %s because not all dependencies loaded: %s',
|
||||
domain, ", ".join(missing_deps))
|
||||
return None
|
||||
|
||||
if hasattr(component, 'CONFIG_SCHEMA'):
|
||||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
return None
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
platforms = []
|
||||
for p_name, p_config in config_per_platform(config, domain):
|
||||
# Validate component specific platform schema
|
||||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
continue
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
# So if p_name is None we are not going to validate platform
|
||||
# (the automation component is one of them)
|
||||
if p_name is None:
|
||||
platforms.append(p_validated)
|
||||
continue
|
||||
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, domain, p_name)
|
||||
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
# Validate platform specific schema
|
||||
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated, hass)
|
||||
continue
|
||||
|
||||
platforms.append(p_validated)
|
||||
|
||||
# Create a copy of the configuration with all config for current
|
||||
# component removed and add validated config back in.
|
||||
filter_keys = extract_domain_configs(config, domain)
|
||||
config = {key: value for key, value in config.items()
|
||||
if key not in filter_keys}
|
||||
config[domain] = platforms
|
||||
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, component, domain)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) -> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_platform(hass, config, domain, platform_name),
|
||||
loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) \
|
||||
-> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
|
||||
|
||||
platform = loader.get_platform(domain, platform_name)
|
||||
|
||||
# Not found
|
||||
if platform is None:
|
||||
_LOGGER.error('Unable to find platform %s', platform_path)
|
||||
_async_persistent_notification(hass, platform_path)
|
||||
return None
|
||||
|
||||
# Already loaded
|
||||
elif platform_path in hass.config.components:
|
||||
return platform
|
||||
|
||||
# Load dependencies
|
||||
for component in getattr(platform, 'DEPENDENCIES', []):
|
||||
res = yield from async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error(
|
||||
'Unable to prepare setup for platform %s because '
|
||||
'dependency %s could not be initialized', platform_path,
|
||||
component)
|
||||
_async_persistent_notification(hass, platform_path, True)
|
||||
return None
|
||||
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, platform, platform_path)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return platform
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction'))
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
@@ -334,23 +49,14 @@ def from_config_dict(config: Dict[str, Any],
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_dict(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.async_add_job(_async_init_from_config_dict(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
)
|
||||
|
||||
return future.result()
|
||||
return hass
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -367,26 +73,22 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
start = time()
|
||||
hass.async_track_tasks()
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
yield from setup_lock.acquire()
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
yield from conf_util.async_process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
if enable_log:
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
@@ -424,18 +126,25 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
# stage 1
|
||||
for component in components:
|
||||
if component not in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
|
||||
# Setup the components
|
||||
for domain in loader.load_order_components(components):
|
||||
yield from _async_setup_component(hass, domain, config)
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
setup_lock.release()
|
||||
# stage 2
|
||||
for component in components:
|
||||
if component in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
|
||||
yield from hass.async_stop_track_tasks()
|
||||
|
||||
stop = time()
|
||||
_LOGGER.info('Home Assistant initialized in %ss', round(stop-start, 2))
|
||||
|
||||
async_register_signal_handling(hass)
|
||||
return hass
|
||||
|
||||
@@ -453,22 +162,13 @@ def from_config_file(config_path: str,
|
||||
if hass is None:
|
||||
hass = core.HomeAssistant()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_file(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.loop.create_task(_async_init_from_config_file(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
)
|
||||
|
||||
return future.result()
|
||||
return hass
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -488,12 +188,13 @@ def async_from_config_file(config_path: str,
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, mount_local_lib_path, config_dir)
|
||||
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
try:
|
||||
config_dict = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError:
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Error loading %s: %s', config_path, err)
|
||||
return None
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
@@ -503,11 +204,12 @@ def async_from_config_file(config_path: str,
|
||||
return hass
|
||||
|
||||
|
||||
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
log_rotate_days=None) -> None:
|
||||
@core.callback
|
||||
def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
log_rotate_days=None) -> None:
|
||||
"""Setup the logging.
|
||||
|
||||
Async friendly.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
@@ -537,10 +239,6 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# AsyncHandler allready exists?
|
||||
if hass.data.get(core.DATA_ASYNCHANDLER):
|
||||
return
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
err_path_exists = os.path.isfile(err_log_path)
|
||||
@@ -561,7 +259,15 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
||||
|
||||
async_handler = AsyncHandler(hass.loop, err_handler)
|
||||
hass.data[core.DATA_ASYNCHANDLER] = async_handler
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_async_handler(event):
|
||||
"""Cleanup async handler."""
|
||||
logging.getLogger('').removeHandler(async_handler)
|
||||
yield from async_handler.async_close(blocking=True)
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(async_handler)
|
||||
@@ -572,57 +278,6 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
'Unable to setup error log %s (access denied)', err_log_path)
|
||||
|
||||
|
||||
def log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_log_exception, ex, domain, config, hass).result()
|
||||
|
||||
|
||||
@core.callback
|
||||
def _async_persistent_notification(hass: core.HomeAssistant, component: str,
|
||||
link: Optional[bool]=False):
|
||||
"""Print a persistent notification.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
_PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link
|
||||
_lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name)
|
||||
if link else name for name, link in _PERSISTENT_ERRORS.items()]
|
||||
message = ('The following components and platforms could not be set up:\n'
|
||||
'* ' + '\n* '.join(list(_lst)) + '\nPlease check your config')
|
||||
persistent_notification.async_create(
|
||||
hass, message, 'Invalid config', 'invalid_config')
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
if hass is not None:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||
.format(ex.path[-1], domain, domain,
|
||||
'->'.join(str(m) for m in ex.path))
|
||||
else:
|
||||
message += '{}.'.format(humanize_error(config, ex))
|
||||
|
||||
domain_config = config.get(domain, config)
|
||||
message += " (See {}, line {}). ".format(
|
||||
getattr(domain_config, '__config_file__', '?'),
|
||||
getattr(domain_config, '__line__', '?'))
|
||||
|
||||
if domain != 'homeassistant':
|
||||
message += ('Please check the docs at '
|
||||
'https://home-assistant.io/components/{}/'.format(domain))
|
||||
|
||||
_LOGGER.error(message)
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
|
||||
@@ -156,10 +156,18 @@ def async_setup(hass, config):
|
||||
return
|
||||
|
||||
try:
|
||||
yield from conf_util.async_check_ha_config_file(hass)
|
||||
errors = yield from conf_util.async_check_ha_config_file(hass)
|
||||
except HomeAssistantError:
|
||||
return
|
||||
|
||||
if errors:
|
||||
notif = get_component('persistent_notification')
|
||||
_LOGGER.error(errors)
|
||||
notif.async_create(
|
||||
hass, "Config error. See dev-info panel for details.",
|
||||
"Config validating", "{0}.check_config".format(ha.DOMAIN))
|
||||
return
|
||||
|
||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ def async_setup(hass, config):
|
||||
if not alarm.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
alarm.async_update_ha_state(True))
|
||||
if hasattr(alarm, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
@@ -4,16 +4,20 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
from os import path
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
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 (
|
||||
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
|
||||
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
|
||||
@@ -22,8 +26,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
|
||||
DEVICES = []
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
|
||||
ATTR_KEYPRESS = 'keypress'
|
||||
ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||
@@ -32,68 +34,75 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def alarm_keypress_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
keypress = service.data.get(ATTR_KEYPRESS)
|
||||
|
||||
_target_devices = [device for device in DEVICES
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
for device in _target_devices:
|
||||
EnvisalinkAlarm.alarm_keypress(device, keypress)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
_code = discovery_info[CONF_CODE]
|
||||
_panic_type = discovery_info[CONF_PANIC]
|
||||
for part_num in _configured_partitions:
|
||||
_device_config_data = PARTITION_SCHEMA(
|
||||
_configured_partitions[part_num])
|
||||
_device = EnvisalinkAlarm(
|
||||
part_num,
|
||||
_device_config_data[CONF_PARTITIONNAME],
|
||||
_code,
|
||||
_panic_type,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
DEVICES.append(_device)
|
||||
configured_partitions = discovery_info['partitions']
|
||||
code = discovery_info[CONF_CODE]
|
||||
panic_type = discovery_info[CONF_PANIC]
|
||||
|
||||
add_devices(DEVICES)
|
||||
devices = []
|
||||
for part_num in configured_partitions:
|
||||
device_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
|
||||
device = EnvisalinkAlarm(
|
||||
hass,
|
||||
part_num,
|
||||
device_config_data[CONF_PARTITIONNAME],
|
||||
code,
|
||||
panic_type,
|
||||
hass.data[DATA_EVL].alarm_state['partition'][part_num],
|
||||
hass.data[DATA_EVL]
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
@callback
|
||||
def alarm_keypress_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
keypress = service.data.get(ATTR_KEYPRESS)
|
||||
|
||||
target_devices = [device for device in devices
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
for device in target_devices:
|
||||
device.async_alarm_keypress(keypress)
|
||||
|
||||
# Register Envisalink specific services
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler,
|
||||
descriptions.get(SERVICE_ALARM_KEYPRESS), schema=ALARM_KEYPRESS_SCHEMA)
|
||||
|
||||
hass.services.register(alarm.DOMAIN, SERVICE_ALARM_KEYPRESS,
|
||||
alarm_keypress_handler,
|
||||
descriptions.get(SERVICE_ALARM_KEYPRESS),
|
||||
schema=ALARM_KEYPRESS_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Representation of an Envisalink-based alarm panel."""
|
||||
|
||||
def __init__(self, partition_number, alarm_name, code, panic_type, info,
|
||||
controller):
|
||||
def __init__(self, hass, partition_number, alarm_name, code, panic_type,
|
||||
info, controller):
|
||||
"""Initialize the alarm panel."""
|
||||
from pydispatch import dispatcher
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
self._panic_type = panic_type
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
||||
dispatcher.connect(
|
||||
self._update_callback, signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(
|
||||
self._update_callback, signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self, partition):
|
||||
"""Update HA state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
@@ -126,39 +135,44 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
state = STATE_ALARM_DISARMED
|
||||
return state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
str(code), self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.disarm_partition(str(self._code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
str(self._code), self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(code), self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(self._code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(self._code), self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(code), self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_away_partition(str(self._code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(self._code), self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
@asyncio.coroutine
|
||||
def async_alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
||||
self.hass.data[DATA_EVL].panic_alarm(self._panic_type)
|
||||
|
||||
def alarm_keypress(self, keypress=None):
|
||||
@callback
|
||||
def async_alarm_keypress(self, keypress=None):
|
||||
"""Send custom keypress."""
|
||||
if keypress:
|
||||
EVL_CONTROLLER.keypresses_to_partition(self._partition_number,
|
||||
keypress)
|
||||
self.hass.data[DATA_EVL].keypresses_to_partition(
|
||||
self._partition_number, keypress)
|
||||
|
||||
@@ -4,10 +4,12 @@ This platform enables the possibility to control a MQTT alarm.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import (
|
||||
@@ -41,10 +43,10 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the MQTT platform."""
|
||||
add_devices([MqttAlarm(
|
||||
hass,
|
||||
async_add_devices([MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
@@ -58,11 +60,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class MqttAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code):
|
||||
def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, code):
|
||||
"""Initalize the MQTT alarm panel."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
@@ -72,6 +73,12 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._code = code
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
@@ -80,9 +87,10 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
_LOGGER.warning('Received unexpected payload: %s', payload)
|
||||
return
|
||||
self._state = payload
|
||||
self.update_ha_state()
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -104,26 +112,38 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
"""One or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_disarm, self._qos)
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_disarm, self._qos)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_home, self._qos)
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_home, self._qos)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
return
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_away, self._qos)
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_away, self._qos)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
|
||||
@@ -12,16 +12,19 @@ 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, STATE_UNKNOWN, CONF_CODE, CONF_NAME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
|
||||
REQUIREMENTS = ['https://github.com/w1ll1am23/simplisafe-python/archive/'
|
||||
'586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#'
|
||||
'simplisafe-python==0.0.1']
|
||||
REQUIREMENTS = ['simplisafe-python==1.0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'SimpliSafe'
|
||||
DOMAIN = 'simplisafe'
|
||||
NOTIFICATION_ID = 'simplisafe_notification'
|
||||
NOTIFICATION_TITLE = 'SimpliSafe Setup'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
@@ -33,33 +36,44 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
from simplipy.api import SimpliSafeApiInterface, get_systems
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
add_devices([SimpliSafeAlarm(name, username, password, code)])
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
simplisafe = SimpliSafeApiInterface()
|
||||
status = simplisafe.set_credentials(username, password)
|
||||
if status:
|
||||
hass.data[DOMAIN] = simplisafe
|
||||
locations = get_systems(simplisafe)
|
||||
for location in locations:
|
||||
add_devices([SimpliSafeAlarm(location, name, code)])
|
||||
else:
|
||||
message = 'Failed to log into SimpliSafe. Check credentials.'
|
||||
_LOGGER.error(message)
|
||||
persistent_notification.create(
|
||||
hass, message,
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
def logout(event):
|
||||
"""Logout of the SimpliSafe API."""
|
||||
hass.data[DOMAIN].logout()
|
||||
|
||||
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, logout)
|
||||
|
||||
|
||||
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, name, username, password, code):
|
||||
def __init__(self, simplisafe, name, code):
|
||||
"""Initialize the SimpliSafe alarm."""
|
||||
from simplisafe import SimpliSafe
|
||||
self.simplisafe = SimpliSafe(username, password)
|
||||
self.simplisafe = simplisafe
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._id = self.simplisafe.get_id()
|
||||
status = self.simplisafe.get_state()
|
||||
if status == 'Off':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif status == 'Home':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'Away':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -67,7 +81,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return 'Alarm {}'.format(self._id)
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
@@ -77,21 +91,32 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
status = self.simplisafe.state()
|
||||
if status == 'Off':
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status == 'Home':
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'Away':
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
state = STATE_UNKNOWN
|
||||
return state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'temperature': self.simplisafe.temperature(),
|
||||
'co': self.simplisafe.carbon_monoxide(),
|
||||
'fire': self.simplisafe.fire(),
|
||||
'alarm': self.simplisafe.alarm(),
|
||||
'last_event': self.simplisafe.last_event(),
|
||||
'flood': self.simplisafe.flood()
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
self.simplisafe.get_location()
|
||||
status = self.simplisafe.get_state()
|
||||
|
||||
if status == 'Off':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif status == 'Home':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'Away':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
self.simplisafe.update()
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -13,13 +13,11 @@ 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)
|
||||
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)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers import service, event
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -32,13 +30,16 @@ CONF_NOTIFIERS = 'notifiers'
|
||||
CONF_REPEAT = 'repeat'
|
||||
CONF_SKIP_FIRST = 'skip_first'
|
||||
|
||||
DEFAULT_CAN_ACK = True
|
||||
DEFAULT_SKIP_FIRST = False
|
||||
|
||||
ALERT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
||||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||
vol.Required(CONF_CAN_ACK, default=True): cv.boolean,
|
||||
vol.Required(CONF_SKIP_FIRST, default=False): cv.boolean,
|
||||
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
|
||||
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
||||
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@@ -60,7 +61,7 @@ def is_on(hass, entity_id):
|
||||
|
||||
def turn_on(hass, entity_id):
|
||||
"""Reset the alert."""
|
||||
run_callback_threadsafe(hass.loop, async_turn_on, hass, entity_id)
|
||||
hass.add_job(async_turn_on, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -73,7 +74,7 @@ def async_turn_on(hass, entity_id):
|
||||
|
||||
def turn_off(hass, entity_id):
|
||||
"""Acknowledge alert."""
|
||||
run_callback_threadsafe(hass.loop, async_turn_off, hass, entity_id)
|
||||
hass.add_job(async_turn_off, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -86,7 +87,7 @@ def async_turn_off(hass, entity_id):
|
||||
|
||||
def toggle(hass, entity_id):
|
||||
"""Toggle acknowledgement of alert."""
|
||||
run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id)
|
||||
hass.add_job(async_toggle, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -99,7 +100,7 @@ def async_toggle(hass, entity_id):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup alert component."""
|
||||
"""Set up the Alert component."""
|
||||
alerts = config.get(DOMAIN)
|
||||
all_alerts = {}
|
||||
|
||||
@@ -117,7 +118,7 @@ def async_setup(hass, config):
|
||||
else:
|
||||
yield from alert.async_turn_off()
|
||||
|
||||
# setup alerts
|
||||
# Setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
entity = Alert(hass, entity_id,
|
||||
alert[CONF_NAME], alert[CONF_ENTITY_ID],
|
||||
@@ -126,13 +127,13 @@ def async_setup(hass, config):
|
||||
alert[CONF_CAN_ACK])
|
||||
all_alerts[entity.entity_id] = entity
|
||||
|
||||
# read descriptions
|
||||
# Read descriptions
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = descriptions.get(DOMAIN, {})
|
||||
|
||||
# setup service calls
|
||||
# Setup service calls
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA)
|
||||
@@ -171,8 +172,8 @@ class Alert(ToggleEntity):
|
||||
self._cancel = None
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
|
||||
|
||||
event.async_track_state_change(hass, watched_entity_id,
|
||||
self.watched_entity_change)
|
||||
event.async_track_state_change(
|
||||
hass, watched_entity_id, self.watched_entity_change)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -201,7 +202,7 @@ class Alert(ToggleEntity):
|
||||
@asyncio.coroutine
|
||||
def watched_entity_change(self, entity, from_state, to_state):
|
||||
"""Determine if the alert should start or stop."""
|
||||
_LOGGER.debug('Watched entity (%s) has changed.', entity)
|
||||
_LOGGER.debug("Watched entity (%s) has changed", entity)
|
||||
if to_state.state == self._alert_state and not self._firing:
|
||||
yield from self.begin_alerting()
|
||||
if to_state.state != self._alert_state and self._firing:
|
||||
@@ -210,7 +211,7 @@ class Alert(ToggleEntity):
|
||||
@asyncio.coroutine
|
||||
def begin_alerting(self):
|
||||
"""Begin the alert procedures."""
|
||||
_LOGGER.debug('Beginning Alert: %s', self._name)
|
||||
_LOGGER.debug("Beginning Alert: %s", self._name)
|
||||
self._ack = False
|
||||
self._firing = True
|
||||
self._next_delay = 0
|
||||
@@ -225,7 +226,7 @@ class Alert(ToggleEntity):
|
||||
@asyncio.coroutine
|
||||
def end_alerting(self):
|
||||
"""End the alert procedures."""
|
||||
_LOGGER.debug('Ending Alert: %s', self._name)
|
||||
_LOGGER.debug("Ending Alert: %s", self._name)
|
||||
self._cancel()
|
||||
self._ack = False
|
||||
self._firing = False
|
||||
@@ -247,7 +248,7 @@ class Alert(ToggleEntity):
|
||||
return
|
||||
|
||||
if not self._ack:
|
||||
_LOGGER.info('Alerting: %s', self._name)
|
||||
_LOGGER.info("Alerting: %s", self._name)
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._name})
|
||||
@@ -256,14 +257,14 @@ class Alert(ToggleEntity):
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self):
|
||||
"""Async Unacknowledge alert."""
|
||||
_LOGGER.debug('Reset Alert: %s', self._name)
|
||||
_LOGGER.debug("Reset Alert: %s", self._name)
|
||||
self._ack = False
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self):
|
||||
"""Async Acknowledge alert."""
|
||||
_LOGGER.debug('Acknowledged Alert: %s', self._name)
|
||||
_LOGGER.debug("Acknowledged Alert: %s", self._name)
|
||||
self._ack = True
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
Support for IP Webcam, an Android app that acts as a full-featured webcam.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/android_ip_webcam/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL,
|
||||
CONF_PLATFORM)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send, async_dispatcher_connect)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
||||
|
||||
DOMAIN = 'android_ip_webcam'
|
||||
REQUIREMENTS = ["pydroid-ipcam==0.6"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
DATA_IP_WEBCAM = 'android_ip_webcam'
|
||||
|
||||
ATTR_HOST = 'host'
|
||||
ATTR_VID_CONNS = 'Video Connections'
|
||||
ATTR_AUD_CONNS = 'Audio Connections'
|
||||
|
||||
KEY_MAP = {
|
||||
'audio_connections': 'Audio Connections',
|
||||
'adet_limit': 'Audio Trigger Limit',
|
||||
'antibanding': 'Anti-banding',
|
||||
'audio_only': 'Audio Only',
|
||||
'battery_level': 'Battery Level',
|
||||
'battery_temp': 'Battery Temperature',
|
||||
'battery_voltage': 'Battery Voltage',
|
||||
'coloreffect': 'Color Effect',
|
||||
'exposure': 'Exposure Level',
|
||||
'exposure_lock': 'Exposure Lock',
|
||||
'ffc': 'Front-facing Camera',
|
||||
'flashmode': 'Flash Mode',
|
||||
'focus': 'Focus',
|
||||
'focus_homing': 'Focus Homing',
|
||||
'focus_region': 'Focus Region',
|
||||
'focusmode': 'Focus Mode',
|
||||
'gps_active': 'GPS Active',
|
||||
'idle': 'Idle',
|
||||
'ip_address': 'IPv4 Address',
|
||||
'ipv6_address': 'IPv6 Address',
|
||||
'ivideon_streaming': 'Ivideon Streaming',
|
||||
'light': 'Light Level',
|
||||
'mirror_flip': 'Mirror Flip',
|
||||
'motion': 'Motion',
|
||||
'motion_active': 'Motion Active',
|
||||
'motion_detect': 'Motion Detection',
|
||||
'motion_event': 'Motion Event',
|
||||
'motion_limit': 'Motion Limit',
|
||||
'night_vision': 'Night Vision',
|
||||
'night_vision_average': 'Night Vision Average',
|
||||
'night_vision_gain': 'Night Vision Gain',
|
||||
'orientation': 'Orientation',
|
||||
'overlay': 'Overlay',
|
||||
'photo_size': 'Photo Size',
|
||||
'pressure': 'Pressure',
|
||||
'proximity': 'Proximity',
|
||||
'quality': 'Quality',
|
||||
'scenemode': 'Scene Mode',
|
||||
'sound': 'Sound',
|
||||
'sound_event': 'Sound Event',
|
||||
'sound_timeout': 'Sound Timeout',
|
||||
'torch': 'Torch',
|
||||
'video_connections': 'Video Connections',
|
||||
'video_chunk_len': 'Video Chunk Length',
|
||||
'video_recording': 'Video Recording',
|
||||
'video_size': 'Video Size',
|
||||
'whitebalance': 'White Balance',
|
||||
'whitebalance_lock': 'White Balance Lock',
|
||||
'zoom': 'Zoom'
|
||||
}
|
||||
|
||||
ICON_MAP = {
|
||||
'audio_connections': 'mdi:speaker',
|
||||
'battery_level': 'mdi:battery',
|
||||
'battery_temp': 'mdi:thermometer',
|
||||
'battery_voltage': 'mdi:battery-charging-100',
|
||||
'exposure_lock': 'mdi:camera',
|
||||
'ffc': 'mdi:camera-front-variant',
|
||||
'focus': 'mdi:image-filter-center-focus',
|
||||
'gps_active': 'mdi:crosshairs-gps',
|
||||
'light': 'mdi:flashlight',
|
||||
'motion': 'mdi:run',
|
||||
'night_vision': 'mdi:weather-night',
|
||||
'overlay': 'mdi:monitor',
|
||||
'pressure': 'mdi:gauge',
|
||||
'proximity': 'mdi:map-marker-radius',
|
||||
'quality': 'mdi:quality-high',
|
||||
'sound': 'mdi:speaker',
|
||||
'sound_event': 'mdi:speaker',
|
||||
'sound_timeout': 'mdi:speaker',
|
||||
'torch': 'mdi:white-balance-sunny',
|
||||
'video_chunk_len': 'mdi:video',
|
||||
'video_connections': 'mdi:eye',
|
||||
'video_recording': 'mdi:record-rec',
|
||||
'whitebalance_lock': 'mdi:white-balance-auto'
|
||||
}
|
||||
|
||||
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision',
|
||||
'overlay', 'torch', 'whitebalance_lock', 'video_recording']
|
||||
|
||||
SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
||||
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
|
||||
'sound', 'video_connections']
|
||||
|
||||
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
||||
|
||||
CONF_MOTION_SENSOR = 'motion_sensor'
|
||||
|
||||
DEFAULT_NAME = 'IP Webcam'
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_SWITCHES, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(CONF_MOTION_SENSOR, default=None): cv.boolean,
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the IP Webcam component."""
|
||||
from pydroid_ipcam import PyDroidIPCam
|
||||
|
||||
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_ipcamera(cam_config):
|
||||
"""Setup a ip camera."""
|
||||
host = cam_config[CONF_HOST]
|
||||
username = cam_config.get(CONF_USERNAME)
|
||||
password = cam_config.get(CONF_PASSWORD)
|
||||
name = cam_config[CONF_NAME]
|
||||
interval = cam_config[CONF_SCAN_INTERVAL]
|
||||
switches = cam_config[CONF_SWITCHES]
|
||||
sensors = cam_config[CONF_SENSORS]
|
||||
motion = cam_config[CONF_MOTION_SENSOR]
|
||||
|
||||
# init ip webcam
|
||||
cam = PyDroidIPCam(
|
||||
hass.loop, websession, host, cam_config[CONF_PORT],
|
||||
username=username, password=password,
|
||||
timeout=cam_config[CONF_TIMEOUT]
|
||||
)
|
||||
|
||||
if switches is None:
|
||||
switches = [setting for setting in cam.enabled_settings
|
||||
if setting in SWITCHES]
|
||||
|
||||
if sensors is None:
|
||||
sensors = [sensor for sensor in cam.enabled_sensors
|
||||
if sensor in SENSORS]
|
||||
sensors.extend(['audio_connections', 'video_connections'])
|
||||
|
||||
if motion is None:
|
||||
motion = 'motion_active' in cam.enabled_sensors
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_data(now):
|
||||
"""Update data from ipcam in SCAN_INTERVAL."""
|
||||
yield from cam.update()
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
||||
|
||||
async_track_point_in_utc_time(
|
||||
hass, async_update_data, utcnow() + interval)
|
||||
|
||||
yield from async_update_data(None)
|
||||
|
||||
# load platforms
|
||||
webcams[host] = cam
|
||||
|
||||
mjpeg_camera = {
|
||||
CONF_PLATFORM: 'mjpeg',
|
||||
CONF_MJPEG_URL: cam.mjpeg_url,
|
||||
CONF_STILL_IMAGE_URL: cam.image_url,
|
||||
CONF_NAME: name,
|
||||
}
|
||||
if username and password:
|
||||
mjpeg_camera.update({
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password
|
||||
})
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'camera', 'mjpeg', mjpeg_camera, config))
|
||||
|
||||
if sensors:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config))
|
||||
|
||||
if switches:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
CONF_SWITCHES: switches,
|
||||
}, config))
|
||||
|
||||
if motion:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
CONF_HOST: host,
|
||||
CONF_NAME: name,
|
||||
}, config))
|
||||
|
||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AndroidIPCamEntity(Entity):
|
||||
"""The Android device running IP Webcam."""
|
||||
|
||||
def __init__(self, host, ipcam):
|
||||
"""Initialize the data oject."""
|
||||
self._host = host
|
||||
self._ipcam = ipcam
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register update dispatcher."""
|
||||
@callback
|
||||
def async_ipcam_update(host):
|
||||
"""Update callback."""
|
||||
if self._host != host:
|
||||
return
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._ipcam.available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {ATTR_HOST: self._host}
|
||||
if self._ipcam.status_data is None:
|
||||
return state_attr
|
||||
|
||||
state_attr[ATTR_VID_CONNS] = \
|
||||
self._ipcam.status_data.get('video_connections')
|
||||
state_attr[ATTR_AUD_CONNS] = \
|
||||
self._ipcam.status_data.get('audio_connections')
|
||||
|
||||
return state_attr
|
||||
@@ -327,6 +327,8 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
_LOGGER.warning('Event forwarding is deprecated. '
|
||||
'Will be removed by 0.43')
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
|
||||
@@ -77,14 +77,14 @@ class ApiaiIntentsView(HomeAssistantView):
|
||||
"""Handle API.AI."""
|
||||
data = yield from request.json()
|
||||
|
||||
_LOGGER.debug('Received Apiai request: %s', data)
|
||||
_LOGGER.debug("Received api.ai request: %s", data)
|
||||
|
||||
req = data.get('result')
|
||||
|
||||
if req is None:
|
||||
_LOGGER.error('Received invalid data from Apiai: %s', data)
|
||||
return self.json_message('Expected result value not received',
|
||||
HTTP_BAD_REQUEST)
|
||||
_LOGGER.error("Received invalid data from api.ai: %s", data)
|
||||
return self.json_message(
|
||||
"Expected result value not received", HTTP_BAD_REQUEST)
|
||||
|
||||
action_incomplete = req['actionIncomplete']
|
||||
|
||||
@@ -106,7 +106,7 @@ class ApiaiIntentsView(HomeAssistantView):
|
||||
# return self.json(response)
|
||||
|
||||
if intent == "":
|
||||
_LOGGER.warning('Received intent with empty action')
|
||||
_LOGGER.warning("Received intent with empty action")
|
||||
response.add_speech(
|
||||
"You have not defined an action in your api.ai intent.")
|
||||
return self.json(response)
|
||||
@@ -114,7 +114,7 @@ class ApiaiIntentsView(HomeAssistantView):
|
||||
config = self.intents.get(intent)
|
||||
|
||||
if config is None:
|
||||
_LOGGER.warning('Received unknown intent %s', intent)
|
||||
_LOGGER.warning("Received unknown intent %s", intent)
|
||||
response.add_speech(
|
||||
"Intent '%s' is not yet configured within Home Assistant." %
|
||||
intent)
|
||||
|
||||
@@ -11,16 +11,17 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import async_prepare_setup_platform
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE)
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD)
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.loader import get_platform
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -28,8 +29,6 @@ import homeassistant.helpers.config_validation as cv
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DEPENDENCIES = ['group']
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
@@ -52,7 +51,6 @@ DEFAULT_INITIAL_STATE = True
|
||||
ATTR_LAST_TRIGGERED = 'last_triggered'
|
||||
ATTR_VARIABLES = 'variables'
|
||||
SERVICE_TRIGGER = 'trigger'
|
||||
SERVICE_RELOAD = 'reload'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -226,7 +224,7 @@ class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||
hidden):
|
||||
hidden, initial_state):
|
||||
"""Initialize an automation entity."""
|
||||
self._name = name
|
||||
self._async_attach_triggers = async_attach_triggers
|
||||
@@ -236,6 +234,7 @@ class AutomationEntity(ToggleEntity):
|
||||
self._enabled = False
|
||||
self._last_triggered = None
|
||||
self._hidden = hidden
|
||||
self._initial_state = initial_state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -264,6 +263,18 @@ class AutomationEntity(ToggleEntity):
|
||||
"""Return True if entity is on."""
|
||||
return self._enabled
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Startup with initial state or previous state."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state is None:
|
||||
if self._initial_state:
|
||||
yield from self.async_enable()
|
||||
else:
|
||||
self._last_triggered = state.attributes.get('last_triggered')
|
||||
if state.state == STATE_ON:
|
||||
yield from self.async_enable()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on and update the state."""
|
||||
@@ -322,7 +333,6 @@ def _async_process_config(hass, config, component):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
entities = []
|
||||
tasks = []
|
||||
|
||||
for config_key in extract_domain_configs(config, DOMAIN):
|
||||
conf = config[config_key]
|
||||
@@ -332,6 +342,7 @@ def _async_process_config(hass, config, component):
|
||||
list_no)
|
||||
|
||||
hidden = config_block[CONF_HIDE_ENTITY]
|
||||
initial_state = config_block[CONF_INITIAL_STATE]
|
||||
|
||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
||||
name)
|
||||
@@ -348,15 +359,14 @@ def _async_process_config(hass, config, component):
|
||||
|
||||
async_attach_triggers = partial(
|
||||
_async_process_trigger, hass, config,
|
||||
config_block.get(CONF_TRIGGER, []), name)
|
||||
entity = AutomationEntity(name, async_attach_triggers, cond_func,
|
||||
action, hidden)
|
||||
if config_block[CONF_INITIAL_STATE]:
|
||||
tasks.append(entity.async_enable())
|
||||
config_block.get(CONF_TRIGGER, []), name
|
||||
)
|
||||
entity = AutomationEntity(
|
||||
name, async_attach_triggers, cond_func, action, hidden,
|
||||
initial_state)
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
@@ -412,7 +422,7 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
if platform is None:
|
||||
return None
|
||||
|
||||
remove = platform.async_trigger(hass, conf, action)
|
||||
remove = yield from platform.async_trigger(hass, conf, action)
|
||||
|
||||
if not remove:
|
||||
_LOGGER.error("Error setting up trigger %s", name)
|
||||
|
||||
@@ -4,6 +4,7 @@ Offer event listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#event-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -24,6 +25,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
|
||||
@@ -4,6 +4,7 @@ Trigger an automation when a LiteJet switch is released.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/automation.litejet/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -32,6 +33,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
number = config.get(CONF_NUMBER)
|
||||
|
||||
@@ -4,6 +4,7 @@ Offer MQTT listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -24,6 +25,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
topic = config.get(CONF_TOPIC)
|
||||
@@ -49,4 +51,6 @@ def async_trigger(hass, config, action):
|
||||
'trigger': data
|
||||
})
|
||||
|
||||
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
||||
remove = yield from mqtt.async_subscribe(
|
||||
hass, topic, mqtt_automation_listener)
|
||||
return remove
|
||||
|
||||
@@ -4,6 +4,7 @@ Offer numeric state listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -26,6 +27,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
@@ -4,6 +4,7 @@ Offer state listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#state-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
@@ -34,6 +35,7 @@ TRIGGER_SCHEMA = vol.All(
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
@@ -43,6 +45,19 @@ def async_trigger(hass, config, action):
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_state_for_listener = None
|
||||
|
||||
@callback
|
||||
def clear_listener():
|
||||
"""Clear all unsub listener."""
|
||||
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
||||
|
||||
# pylint: disable=not-callable
|
||||
if async_remove_state_for_listener is not None:
|
||||
async_remove_state_for_listener()
|
||||
async_remove_state_for_listener = None
|
||||
if async_remove_state_for_cancel is not None:
|
||||
async_remove_state_for_cancel()
|
||||
async_remove_state_for_cancel = None
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
@@ -64,18 +79,11 @@ def async_trigger(hass, config, action):
|
||||
call_action()
|
||||
return
|
||||
|
||||
@callback
|
||||
def clear_listener():
|
||||
"""Clear all unsub listener."""
|
||||
nonlocal async_remove_state_for_cancel
|
||||
nonlocal async_remove_state_for_listener
|
||||
async_remove_state_for_listener = None
|
||||
async_remove_state_for_cancel = None
|
||||
|
||||
@callback
|
||||
def state_for_listener(now):
|
||||
"""Fire on state changes after a delay and calls action."""
|
||||
async_remove_state_for_cancel()
|
||||
nonlocal async_remove_state_for_listener
|
||||
async_remove_state_for_listener = None
|
||||
clear_listener()
|
||||
call_action()
|
||||
|
||||
@@ -84,10 +92,11 @@ def async_trigger(hass, config, action):
|
||||
"""Fire on changes and cancel for listener if changed."""
|
||||
if inner_to_s.state == to_s.state:
|
||||
return
|
||||
async_remove_state_for_listener()
|
||||
async_remove_state_for_cancel()
|
||||
clear_listener()
|
||||
|
||||
# cleanup previous listener
|
||||
clear_listener()
|
||||
|
||||
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||
|
||||
@@ -97,14 +106,10 @@ def async_trigger(hass, config, action):
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
||||
@callback
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
# pylint: disable=not-callable
|
||||
if async_remove_state_for_cancel is not None:
|
||||
async_remove_state_for_cancel()
|
||||
|
||||
if async_remove_state_for_listener is not None:
|
||||
async_remove_state_for_listener()
|
||||
clear_listener()
|
||||
|
||||
return async_remove
|
||||
|
||||
@@ -4,6 +4,7 @@ Offer sun based automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#sun-trigger
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -26,6 +27,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
@@ -4,14 +4,14 @@ Offer template automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#template-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.event import async_track_template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
@@ -23,33 +23,22 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = hass
|
||||
|
||||
# Local variable to keep track of if the action has already been triggered
|
||||
already_triggered = False
|
||||
|
||||
@callback
|
||||
def state_changed_listener(entity_id, from_s, to_s):
|
||||
def template_listener(entity_id, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal already_triggered
|
||||
template_result = condition.async_template(hass, value_template)
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
},
|
||||
})
|
||||
|
||||
# Check to see if template returns true
|
||||
if template_result and not already_triggered:
|
||||
already_triggered = True
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
},
|
||||
})
|
||||
elif not template_result:
|
||||
already_triggered = False
|
||||
|
||||
return async_track_state_change(hass, value_template.extract_entities(),
|
||||
state_changed_listener)
|
||||
return async_track_template(hass, value_template, template_listener)
|
||||
|
||||
@@ -4,6 +4,7 @@ Offer time listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#time-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -29,6 +30,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
CONF_SECONDS, CONF_AFTER))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AFTER in config:
|
||||
|
||||
@@ -4,6 +4,7 @@ Offer zone automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#zone-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
@@ -26,6 +27,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
@@ -14,13 +14,13 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF)
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.helpers.deprecation import deprecated_substitute
|
||||
|
||||
DOMAIN = 'binary_sensor'
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
SENSOR_CLASSES = [
|
||||
None, # Generic on/off
|
||||
DEVICE_CLASSES = [
|
||||
'cold', # On means cold (or too cold)
|
||||
'connectivity', # On means connection present, Off = no connection
|
||||
'gas', # CO, CO2, etc.
|
||||
@@ -38,7 +38,7 @@ SENSOR_CLASSES = [
|
||||
'vibration', # On means vibration detected, Off means no vibration
|
||||
]
|
||||
|
||||
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES))
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -66,16 +66,7 @@ class BinarySensorDevice(Entity):
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
@deprecated_substitute('sensor_class')
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {}
|
||||
|
||||
if self.sensor_class is not None:
|
||||
attr['sensor_class'] = self.sensor_class
|
||||
|
||||
return attr
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Support for IP Webcam binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.android_ip_webcam/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.android_ip_webcam import (
|
||||
KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME)
|
||||
|
||||
DEPENDENCIES = ['android_ip_webcam']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup IP Webcam binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
host = discovery_info[CONF_HOST]
|
||||
name = discovery_info[CONF_NAME]
|
||||
ipcam = hass.data[DATA_IP_WEBCAM][host]
|
||||
|
||||
async_add_devices(
|
||||
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)
|
||||
|
||||
|
||||
class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
||||
"""Represents an IP Webcam binary sensor."""
|
||||
|
||||
def __init__(self, name, host, ipcam, sensor):
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(host, ipcam)
|
||||
|
||||
self._sensor = sensor
|
||||
self._mapped_name = KEY_MAP.get(self._sensor, self._sensor)
|
||||
self._name = '{} {}'.format(name, self._mapped_name)
|
||||
self._state = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor, if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
state, _ = self._ipcam.export_sensor(self._sensor)
|
||||
self._state = state == 1.0
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'motion'
|
||||
@@ -11,11 +11,12 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_SENSOR_CLASS)
|
||||
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_SENSOR_CLASS, CONF_DEVICE_CLASS)
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,7 +26,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_PIN): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
@@ -33,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the aREST binary sensor."""
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
pin = config.get(CONF_PIN)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
|
||||
try:
|
||||
response = requests.get(resource, timeout=10).json()
|
||||
@@ -49,18 +51,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices([ArestBinarySensor(
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
sensor_class, pin)])
|
||||
device_class, pin)])
|
||||
|
||||
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
"""Implement an aREST binary sensor for a pin."""
|
||||
|
||||
def __init__(self, arest, resource, name, sensor_class, pin):
|
||||
def __init__(self, arest, resource, name, device_class, pin):
|
||||
"""Initialize the aREST device."""
|
||||
self.arest = arest
|
||||
self._resource = resource
|
||||
self._name = name
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._pin = pin
|
||||
self.update()
|
||||
|
||||
@@ -81,9 +83,9 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
return bool(self.arest.data.get('state'))
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from aREST API."""
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
Support for aurora forecast data sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.aurora/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor \
|
||||
import (BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
CONF_THRESHOLD = "forecast_threshold"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Aurora Visibility'
|
||||
DEFAULT_DEVICE_CLASS = "visible"
|
||||
DEFAULT_THRESHOLD = 75
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the aurora sensor."""
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Lat. or long. not set in Home Assistant config")
|
||||
return False
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
threshold = config.get(CONF_THRESHOLD)
|
||||
|
||||
try:
|
||||
aurora_data = AuroraData(
|
||||
hass.config.latitude,
|
||||
hass.config.longitude,
|
||||
threshold
|
||||
)
|
||||
aurora_data.update()
|
||||
except requests.exceptions.HTTPError as error:
|
||||
_LOGGER.error(
|
||||
"Connection to aurora forecast service failed: %s", error)
|
||||
return False
|
||||
|
||||
add_devices([AuroraSensor(aurora_data, name)], True)
|
||||
|
||||
|
||||
class AuroraSensor(BinarySensorDevice):
|
||||
"""Implementation of an aurora sensor."""
|
||||
|
||||
def __init__(self, aurora_data, name):
|
||||
"""Initialize the sensor."""
|
||||
self.aurora_data = aurora_data
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return '{}'.format(self._name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if aurora is visible."""
|
||||
return self.aurora_data.is_visible if self.aurora_data else False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device."""
|
||||
return DEFAULT_DEVICE_CLASS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {}
|
||||
|
||||
if self.aurora_data:
|
||||
attrs["visibility_level"] = self.aurora_data.visibility_level
|
||||
attrs["message"] = self.aurora_data.is_visible_text
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from Aurora API and updates the states."""
|
||||
self.aurora_data.update()
|
||||
|
||||
|
||||
class AuroraData(object):
|
||||
"""Get aurora forecast."""
|
||||
|
||||
def __init__(self, latitude, longitude, threshold):
|
||||
"""Initialize the data object."""
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.number_of_latitude_intervals = 513
|
||||
self.number_of_longitude_intervals = 1024
|
||||
self.api_url = \
|
||||
"http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
|
||||
self.headers = {"User-Agent": "Home Assistant Aurora Tracker v.0.1.0"}
|
||||
|
||||
self.threshold = int(threshold)
|
||||
self.is_visible = None
|
||||
self.is_visible_text = None
|
||||
self.visibility_level = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from the Aurora service."""
|
||||
try:
|
||||
self.visibility_level = self.get_aurora_forecast()
|
||||
if int(self.visibility_level) > self.threshold:
|
||||
self.is_visible = True
|
||||
self.is_visible_text = "visible!"
|
||||
else:
|
||||
self.is_visible = False
|
||||
self.is_visible_text = "nothing's out"
|
||||
|
||||
except requests.exceptions.HTTPError as error:
|
||||
_LOGGER.error(
|
||||
"Connection to aurora forecast service failed: %s", error)
|
||||
return False
|
||||
|
||||
def get_aurora_forecast(self):
|
||||
"""Get forecast data and parse for given long/lat."""
|
||||
raw_data = requests.get(self.api_url, headers=self.headers).text
|
||||
forecast_table = [
|
||||
row.strip(" ").split(" ")
|
||||
for row in raw_data.split("\n")
|
||||
if not row.startswith("#")
|
||||
]
|
||||
|
||||
# convert lat and long for data points in table
|
||||
converted_latitude = round((self.latitude / 180)
|
||||
* self.number_of_latitude_intervals)
|
||||
converted_longitude = round((self.longitude / 360)
|
||||
* self.number_of_longitude_intervals)
|
||||
|
||||
return forecast_table[converted_latitude][converted_longitude]
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Support for Blink system camera control.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.blink/
|
||||
"""
|
||||
from homeassistant.components.blink import DOMAIN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the blink binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[DOMAIN].blink
|
||||
devs = list()
|
||||
for name in data.cameras:
|
||||
devs.append(BlinkCameraMotionSensor(name, data))
|
||||
devs.append(BlinkSystemSensor(data))
|
||||
add_devices(devs, True)
|
||||
|
||||
|
||||
class BlinkCameraMotionSensor(BinarySensorDevice):
|
||||
"""A representation of a Blink binary sensor."""
|
||||
|
||||
def __init__(self, name, data):
|
||||
"""Initialize the sensor."""
|
||||
self._name = 'blink_' + name + '_motion_enabled'
|
||||
self._camera_name = name
|
||||
self.data = data
|
||||
self._state = self.data.cameras[self._camera_name].armed
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blink sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update sensor state."""
|
||||
self.data.refresh()
|
||||
self._state = self.data.cameras[self._camera_name].armed
|
||||
|
||||
|
||||
class BlinkSystemSensor(BinarySensorDevice):
|
||||
"""A representation of a Blink system sensor."""
|
||||
|
||||
def __init__(self, data):
|
||||
"""Initialize the sensor."""
|
||||
self._name = 'blink armed status'
|
||||
self.data = data
|
||||
self._state = self.data.arm
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blink sensor."""
|
||||
return self._name.replace(" ", "_")
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update sensor state."""
|
||||
self.data.refresh()
|
||||
self._state = self.data.arm
|
||||
@@ -64,8 +64,8 @@ class BloomSkySensor(BinarySensorDevice):
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
|
||||
@@ -10,12 +10,13 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.sensor.command_line import CommandSensorData
|
||||
from homeassistant.const import (
|
||||
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSOR_CLASS, CONF_COMMAND)
|
||||
CONF_SENSOR_CLASS, CONF_COMMAND, CONF_DEVICE_CLASS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,7 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
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_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
})
|
||||
|
||||
@@ -42,27 +44,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
command = config.get(CONF_COMMAND)
|
||||
payload_off = config.get(CONF_PAYLOAD_OFF)
|
||||
payload_on = config.get(CONF_PAYLOAD_ON)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
data = CommandSensorData(command)
|
||||
|
||||
add_devices([CommandBinarySensor(
|
||||
hass, data, name, sensor_class, payload_on, payload_off,
|
||||
hass, data, name, device_class, payload_on, payload_off,
|
||||
value_template)])
|
||||
|
||||
|
||||
class CommandBinarySensor(BinarySensorDevice):
|
||||
"""Represent a command line binary sensor."""
|
||||
|
||||
def __init__(self, hass, data, name, sensor_class, payload_on,
|
||||
def __init__(self, hass, data, name, device_class, payload_on,
|
||||
payload_off, value_template):
|
||||
"""Initialize the Command line binary sensor."""
|
||||
self._hass = hass
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._state = False
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
@@ -80,9 +82,9 @@ class CommandBinarySensor(BinarySensorDevice):
|
||||
return self._state
|
||||
|
||||
@ property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
|
||||
@@ -11,7 +11,7 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES)
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -30,7 +30,7 @@ DEFAULT_SSL = False
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=1)
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||
cv.positive_int: vol.In(DEVICE_CLASSES),
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -102,8 +102,8 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@property
|
||||
|
||||
@@ -18,14 +18,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class DemoBinarySensor(BinarySensorDevice):
|
||||
"""A Demo binary sensor."""
|
||||
|
||||
def __init__(self, name, state, sensor_class):
|
||||
def __init__(self, name, state, device_class):
|
||||
"""Initialize the demo sensor."""
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._sensor_type = sensor_class
|
||||
self._sensor_type = device_class
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
dev = []
|
||||
for droplet in droplets:
|
||||
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
|
||||
if droplet_id is None:
|
||||
_LOGGER.error("Droplet %s is not available", droplet)
|
||||
return False
|
||||
dev.append(DigitalOceanBinarySensor(
|
||||
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||
|
||||
@@ -63,7 +66,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
return self.data.status == 'active'
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEFAULT_SENSOR_CLASS
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
||||
self.sensor_name = sensor_name
|
||||
self.index = sensor_index
|
||||
self._state = None
|
||||
self._sensor_class = 'occupancy'
|
||||
self._device_class = 'occupancy'
|
||||
self.update()
|
||||
|
||||
@property
|
||||
@@ -57,9 +57,9 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
||||
return "binary_sensor_ecobee_{}_{}".format(self._name, self.index)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._sensor_class
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
|
||||
@@ -9,10 +9,12 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.components import enocean
|
||||
from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SENSOR_CLASS)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ID, CONF_SENSOR_CLASS, CONF_DEVICE_CLASS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,7 +24,8 @@ DEFAULT_NAME = 'EnOcean binary sensor'
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
@@ -30,15 +33,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Binary Sensor platform fo EnOcean."""
|
||||
dev_id = config.get(CONF_ID)
|
||||
devname = config.get(CONF_NAME)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
|
||||
add_devices([EnOceanBinarySensor(dev_id, devname, sensor_class)])
|
||||
add_devices([EnOceanBinarySensor(dev_id, devname, device_class)])
|
||||
|
||||
|
||||
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
"""Representation of EnOcean binary sensors such as wall switches."""
|
||||
|
||||
def __init__(self, dev_id, devname, sensor_class):
|
||||
def __init__(self, dev_id, devname, device_class):
|
||||
"""Initialize the EnOcean binary sensor."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.stype = "listener"
|
||||
@@ -46,7 +49,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
self.which = -1
|
||||
self.onoff = -1
|
||||
self.devname = devname
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -54,9 +57,9 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
return self.devname
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
def value_changed(self, value, value2):
|
||||
"""Fire an event with the data that have changed.
|
||||
@@ -64,7 +67,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
This method is called when there is an incoming packet associated
|
||||
with this platform.
|
||||
"""
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
if value2 == 0x70:
|
||||
self.which = 0
|
||||
self.onoff = 0
|
||||
|
||||
@@ -4,48 +4,59 @@ Support for Envisalink zone states- represented as binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.envisalink/
|
||||
"""
|
||||
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.envisalink import (EVL_CONTROLLER,
|
||||
ZONE_SCHEMA,
|
||||
CONF_ZONENAME,
|
||||
CONF_ZONETYPE,
|
||||
EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.components.envisalink import (
|
||||
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup Envisalink binary sensor devices."""
|
||||
_configured_zones = discovery_info['zones']
|
||||
for zone_num in _configured_zones:
|
||||
_device_config_data = ZONE_SCHEMA(_configured_zones[zone_num])
|
||||
_device = EnvisalinkBinarySensor(
|
||||
configured_zones = discovery_info['zones']
|
||||
|
||||
devices = []
|
||||
for zone_num in configured_zones:
|
||||
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
|
||||
device = EnvisalinkBinarySensor(
|
||||
hass,
|
||||
zone_num,
|
||||
_device_config_data[CONF_ZONENAME],
|
||||
_device_config_data[CONF_ZONETYPE],
|
||||
EVL_CONTROLLER.alarm_state['zone'][zone_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
device_config_data[CONF_ZONENAME],
|
||||
device_config_data[CONF_ZONETYPE],
|
||||
hass.data[DATA_EVL].alarm_state['zone'][zone_num],
|
||||
hass.data[DATA_EVL]
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""Representation of an Envisalink binary sensor."""
|
||||
|
||||
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
||||
def __init__(self, hass, zone_number, zone_name, zone_type, info,
|
||||
controller):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
self._zone_type = zone_type
|
||||
self._zone_number = zone_number
|
||||
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
EnvisalinkDevice.__init__(self, zone_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
super().__init__(zone_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -60,11 +71,12 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
return self._info['status']['open']
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@callback
|
||||
def _update_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self.hass.async_add_job(self.update_ha_state)
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@@ -57,16 +57,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegMotion(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Constructor for binary sensor noise detection."""
|
||||
super().__init__(config.get(CONF_INITIAL_STATE))
|
||||
|
||||
@@ -98,15 +95,19 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||
"""Initialize ffmpeg motion binary sensor."""
|
||||
from haffmpeg import SensorMotion
|
||||
|
||||
super().__init__(hass, config)
|
||||
super().__init__(config)
|
||||
self.ffmpeg = SensorMotion(
|
||||
manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_start_ffmpeg(self, entity_ids):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if entity_ids is not None and self.entity_id not in entity_ids:
|
||||
return
|
||||
|
||||
# init config
|
||||
self.ffmpeg.set_options(
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
@@ -116,12 +117,12 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
yield from self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return "motion"
|
||||
|
||||
@@ -54,10 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegNoise(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegNoise(FFmpegBinarySensor):
|
||||
@@ -67,15 +64,19 @@ class FFmpegNoise(FFmpegBinarySensor):
|
||||
"""Initialize ffmpeg noise binary sensor."""
|
||||
from haffmpeg import SensorNoise
|
||||
|
||||
super().__init__(hass, config)
|
||||
super().__init__(config)
|
||||
self.ffmpeg = SensorNoise(
|
||||
manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_start_ffmpeg(self, entity_ids):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if entity_ids is not None and self.entity_id not in entity_ids:
|
||||
return
|
||||
|
||||
# init config
|
||||
self.ffmpeg.set_options(
|
||||
time_duration=self._config.get(CONF_DURATION),
|
||||
@@ -84,13 +85,13 @@ class FFmpegNoise(FFmpegBinarySensor):
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
yield from self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
output_dest=self._config.get(CONF_OUTPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return "sound"
|
||||
|
||||
@@ -179,12 +179,9 @@ class FlicButton(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = super(FlicButton, self).state_attributes
|
||||
attr["address"] = self.address
|
||||
|
||||
return attr
|
||||
return {"address": self.address}
|
||||
|
||||
def _queued_event_check(self, click_type, time_diff):
|
||||
"""Generate a log message and returns true if timeout exceeded."""
|
||||
|
||||
@@ -15,9 +15,10 @@ from homeassistant.components.binary_sensor import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.0.7', 'pydispatcher==2.0.5']
|
||||
REQUIREMENTS = ['pyhik==0.1.0']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
@@ -29,7 +30,7 @@ DEFAULT_DELAY = 0
|
||||
|
||||
ATTR_DELAY = 'delay'
|
||||
|
||||
SENSOR_CLASS_MAP = {
|
||||
DEVICE_CLASS_MAP = {
|
||||
'Motion': 'motion',
|
||||
'Line Crossing': 'motion',
|
||||
'IO Trigger': None,
|
||||
@@ -119,30 +120,32 @@ class HikvisionData(object):
|
||||
self._password = password
|
||||
|
||||
# Establish camera
|
||||
self._cam = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
self.camdata = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
|
||||
if self._name is None:
|
||||
self._name = self._cam.get_name
|
||||
|
||||
# Start event stream
|
||||
self._cam.start_stream()
|
||||
self._name = self.camdata.get_name
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_hik)
|
||||
|
||||
def stop_hik(self, event):
|
||||
"""Shutdown Hikvision subscriptions and subscription thread on exit."""
|
||||
self._cam.disconnect()
|
||||
self.camdata.disconnect()
|
||||
|
||||
def start_hik(self, event):
|
||||
"""Start Hikvision event stream thread."""
|
||||
self.camdata.start_stream()
|
||||
|
||||
@property
|
||||
def sensors(self):
|
||||
"""Return list of available sensors and their states."""
|
||||
return self._cam.current_event_states
|
||||
return self.camdata.current_event_states
|
||||
|
||||
@property
|
||||
def cam_id(self):
|
||||
"""Return camera id."""
|
||||
return self._cam.get_id
|
||||
return self.camdata.get_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -155,8 +158,6 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, hass, sensor, cam, delay):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
|
||||
self._hass = hass
|
||||
self._cam = cam
|
||||
self._name = self._cam.name + ' ' + sensor
|
||||
@@ -170,12 +171,8 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
self._timer = None
|
||||
|
||||
# Form signal for dispatcher
|
||||
signal = 'ValueChanged.{}'.format(self._cam.cam_id)
|
||||
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=signal,
|
||||
sender=self._sensor)
|
||||
# Register callback function with pyHik
|
||||
self._cam.camdata.add_update_callback(self._update_callback, self._id)
|
||||
|
||||
def _sensor_state(self):
|
||||
"""Extract sensor state."""
|
||||
@@ -201,10 +198,10 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
return self._sensor_state()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
try:
|
||||
return SENSOR_CLASS_MAP[self._sensor]
|
||||
return DEVICE_CLASS_MAP[self._sensor]
|
||||
except KeyError:
|
||||
# Sensor must be unknown to us, add as generic
|
||||
return None
|
||||
@@ -225,13 +222,9 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
return attr
|
||||
|
||||
def _update_callback(self, signal, sender):
|
||||
def _update_callback(self, msg):
|
||||
"""Update the sensor's state, if needed."""
|
||||
_LOGGER.debug('Dispatcher callback, signal: %s, sender: %s',
|
||||
signal, sender)
|
||||
|
||||
if sender is not self._sensor:
|
||||
return
|
||||
_LOGGER.debug('Callback signal from: %s', msg)
|
||||
|
||||
if self._delay > 0 and not self.is_on:
|
||||
# Set timer to wait until updating the state
|
||||
|
||||
@@ -7,8 +7,7 @@ https://home-assistant.io/components/binary_sensor.homematic/
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,18 +28,18 @@ SENSOR_TYPES_CLASS = {
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Homematic binary sensor platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMBinarySensor,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, config)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
@@ -54,11 +53,8 @@ class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
return bool(self._hm_get_state())
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
if not self.available:
|
||||
return None
|
||||
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
# If state is MOTION (RemoteMotion works only)
|
||||
if self._state == "MOTION":
|
||||
return "motion"
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Support for INSTEON dimmers via PowerLinc Modem.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/insteon_plm/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['insteon_plm']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the INSTEON PLM device class for the hass platform."""
|
||||
plm = hass.data['insteon_plm']
|
||||
|
||||
device_list = []
|
||||
for device in discovery_info:
|
||||
name = device.get('address')
|
||||
address = device.get('address_hex')
|
||||
|
||||
_LOGGER.info('Registered %s with binary_sensor platform.', name)
|
||||
|
||||
device_list.append(
|
||||
InsteonPLMBinarySensorDevice(hass, plm, address, name)
|
||||
)
|
||||
|
||||
hass.async_add_job(async_add_devices(device_list))
|
||||
|
||||
|
||||
class InsteonPLMBinarySensorDevice(BinarySensorDevice):
|
||||
"""A Class for an Insteon device."""
|
||||
|
||||
def __init__(self, hass, plm, address, name):
|
||||
"""Initialize the binarysensor."""
|
||||
self._hass = hass
|
||||
self._plm = plm.protocol
|
||||
self._address = address
|
||||
self._name = name
|
||||
|
||||
self._plm.add_update_callback(
|
||||
self.async_binarysensor_update, {'address': self._address})
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the the address of the node."""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the the name of the node."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
sensorstate = self._plm.get_device_attr(self._address, 'sensorstate')
|
||||
_LOGGER.info('sensor state for %s is %s', self._address, sensorstate)
|
||||
return bool(sensorstate)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Provide attributes for display on device card."""
|
||||
insteon_plm = get_component('insteon_plm')
|
||||
return insteon_plm.common_attributes(self)
|
||||
|
||||
def get_attr(self, key):
|
||||
"""Return specified attribute for this device."""
|
||||
return self._plm.get_device_attr(self.address, key)
|
||||
|
||||
@callback
|
||||
def async_binarysensor_update(self, message):
|
||||
"""Receive notification from transport that new data exists."""
|
||||
_LOGGER.info('Received update calback from PLM for %s', self._address)
|
||||
self._hass.async_add_job(self.async_update_ha_state())
|
||||
@@ -26,7 +26,7 @@ ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space'
|
||||
CONF_SHOW_ON_MAP = 'show_on_map'
|
||||
|
||||
DEFAULT_NAME = 'ISS'
|
||||
DEFAULT_SENSOR_CLASS = 'visible'
|
||||
DEFAULT_DEVICE_CLASS = 'visible'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
@@ -77,9 +77,9 @@ class IssBinarySensor(BinarySensorDevice):
|
||||
return self.iss_data.is_above if self.iss_data else False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEFAULT_SENSOR_CLASS
|
||||
return DEFAULT_DEVICE_CLASS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Support for MAX! Window Shutter via MAX! Cube.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/maxcube/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add window shutters to HASS."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
|
||||
# List of devices
|
||||
devices = []
|
||||
|
||||
for device in cube.devices:
|
||||
# Create device name by concatenating room name + device name
|
||||
name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
# Only add Window Shutters
|
||||
if cube.is_windowshutter(device):
|
||||
# add device to HASS
|
||||
devices.append(MaxCubeShutter(hass, name, device.rf_address))
|
||||
|
||||
if len(devices) > 0:
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class MaxCubeShutter(BinarySensorDevice):
|
||||
"""MAX! Cube BinarySensor device."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
"""Initialize MAX! Cube BinarySensorDevice."""
|
||||
self._name = name
|
||||
self._sensor_type = 'opening'
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is required."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the BinarySensorDevice."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on/open."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get latest data from MAX! Cube."""
|
||||
self._cubehandle.update()
|
||||
|
||||
# Get the device we want to update
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Update our internal state
|
||||
self._state = device.is_open
|
||||
@@ -4,6 +4,7 @@ Support for MQTT binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -11,12 +12,13 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES)
|
||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
|
||||
CONF_SENSOR_CLASS)
|
||||
CONF_SENSOR_CLASS, CONF_DEVICE_CLASS)
|
||||
from homeassistant.components.mqtt import (CONF_STATE_TOPIC, CONF_QOS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,13 +31,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
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_SENSOR_CLASS, default=None):
|
||||
vol.Any(vol.In(SENSOR_CLASSES), vol.SetTo(None)),
|
||||
vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT binary sensor."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
@@ -43,11 +45,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
add_devices([MqttBinarySensor(
|
||||
hass,
|
||||
|
||||
async_add_devices([MqttBinarySensor(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_SENSOR_CLASS),
|
||||
get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
@@ -58,32 +60,38 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class MqttBinarySensor(BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
def __init__(self, hass, name, state_topic, sensor_class, qos, payload_on,
|
||||
def __init__(self, name, state_topic, device_class, qos, payload_on,
|
||||
payload_off, value_template):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._state_topic = state_topic
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._qos = qos
|
||||
self._template = value_template
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._payload_on:
|
||||
self._state = True
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._payload_off:
|
||||
self._state = False
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -101,6 +109,6 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.mysensors/
|
||||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.binary_sensor import (SENSOR_CLASSES,
|
||||
from homeassistant.components.binary_sensor import (DEVICE_CLASSES,
|
||||
BinarySensorDevice)
|
||||
from homeassistant.const import STATE_ON
|
||||
|
||||
@@ -62,8 +62,8 @@ class MySensorsBinarySensor(
|
||||
return False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
pres = self.gateway.const.Presentation
|
||||
class_map = {
|
||||
pres.S_DOOR: 'opening',
|
||||
@@ -78,5 +78,5 @@ class MySensorsBinarySensor(
|
||||
pres.S_VIBRATION: 'vibration',
|
||||
pres.S_MOISTURE: 'moisture',
|
||||
})
|
||||
if class_map.get(self.child_type) in SENSOR_CLASSES:
|
||||
if class_map.get(self.child_type) in DEVICE_CLASSES:
|
||||
return class_map.get(self.child_type)
|
||||
|
||||
@@ -154,8 +154,8 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
if self._cameratype == "NACamera":
|
||||
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
|
||||
elif self._cameratype == "NOC":
|
||||
|
||||
@@ -12,7 +12,7 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
SENSOR_CLASSES, BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
DEVICE_CLASSES, BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -28,7 +28,7 @@ DEFAULT_PORT = '5007'
|
||||
DEFAULT_SSL = False
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||
cv.positive_int: vol.In(DEVICE_CLASSES),
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -85,8 +85,8 @@ class NX584ZoneSensor(BinarySensorDevice):
|
||||
self._zone_type = zone_type
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@property
|
||||
|
||||
@@ -99,8 +99,8 @@ class OctoPrintBinarySensor(BinarySensorDevice):
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
|
||||
@@ -10,14 +10,15 @@ import voluptuous as vol
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.sensor.rest import RestData
|
||||
from homeassistant.const import (
|
||||
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
|
||||
CONF_SENSOR_CLASS, CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
|
||||
HTTP_DIGEST_AUTHENTICATION)
|
||||
HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -34,7 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
||||
@@ -51,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
headers = config.get(CONF_HEADERS)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
@@ -72,18 +74,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return False
|
||||
|
||||
add_devices([RestBinarySensor(
|
||||
hass, rest, name, sensor_class, value_template)])
|
||||
hass, rest, name, device_class, value_template)])
|
||||
|
||||
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a REST binary sensor."""
|
||||
|
||||
def __init__(self, hass, rest, name, sensor_class, value_template):
|
||||
def __init__(self, hass, rest, name, device_class, value_template):
|
||||
"""Initialize a REST binary sensor."""
|
||||
self._hass = hass
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._state = False
|
||||
self._previous_data = None
|
||||
self._value_template = value_template
|
||||
@@ -95,9 +97,9 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -42,7 +42,7 @@ class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
|
||||
return self._state is True
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return "occupancy"
|
||||
|
||||
|
||||
@@ -12,14 +12,17 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||
SENSOR_CLASSES_SCHEMA)
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS)
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS,
|
||||
EVENT_HOMEASSISTANT_START, STATE_ON)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,7 +30,8 @@ SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA
|
||||
vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -45,7 +49,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
entity_ids = (device_config.get(ATTR_ENTITY_ID) or
|
||||
value_template.extract_entities())
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||
sensor_class = device_config.get(CONF_SENSOR_CLASS)
|
||||
device_class = get_deprecated(
|
||||
device_config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
@@ -55,7 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
hass,
|
||||
device,
|
||||
friendly_name,
|
||||
sensor_class,
|
||||
device_class,
|
||||
value_template,
|
||||
entity_ids)
|
||||
)
|
||||
@@ -63,31 +68,47 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
|
||||
yield from async_add_devices(sensors, True)
|
||||
async_add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
class BinarySensorTemplate(BinarySensorDevice):
|
||||
"""A virtual binary sensor that triggers from another sensor."""
|
||||
|
||||
def __init__(self, hass, device, friendly_name, sensor_class,
|
||||
def __init__(self, hass, device, friendly_name, device_class,
|
||||
value_template, entity_ids):
|
||||
"""Initialize the Template binary sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
hass=hass)
|
||||
self._name = friendly_name
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
self._entities = entity_ids
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._state = state.state == STATE_ON
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
async_track_state_change(
|
||||
hass, entity_ids, template_bsensor_state_listener)
|
||||
@callback
|
||||
def template_bsensor_startup(event):
|
||||
"""Update template on startup."""
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_bsensor_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -100,9 +121,9 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -11,11 +11,12 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, CONF_SENSOR_CLASS,
|
||||
ATTR_ENTITY_ID)
|
||||
ATTR_ENTITY_ID, CONF_DEVICE_CLASS)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -37,7 +38,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
@@ -48,11 +50,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
name = config.get(CONF_NAME)
|
||||
threshold = config.get(CONF_THRESHOLD)
|
||||
limit_type = config.get(CONF_TYPE)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
|
||||
yield from async_add_devices(
|
||||
async_add_devices(
|
||||
[ThresholdSensor(hass, entity_id, name, threshold, limit_type,
|
||||
sensor_class)], True)
|
||||
device_class)], True)
|
||||
return True
|
||||
|
||||
|
||||
@@ -60,14 +62,14 @@ class ThresholdSensor(BinarySensorDevice):
|
||||
"""Representation of a Threshold sensor."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, threshold, limit_type,
|
||||
sensor_class):
|
||||
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._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._deviation = False
|
||||
self.sensor_value = 0
|
||||
|
||||
@@ -105,9 +107,9 @@ class ThresholdSensor(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
||||
@@ -15,12 +15,14 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice,
|
||||
ENTITY_ID_FORMAT,
|
||||
PLATFORM_SCHEMA,
|
||||
SENSOR_CLASSES_SCHEMA)
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_SENSOR_CLASS,
|
||||
CONF_DEVICE_CLASS,
|
||||
STATE_UNKNOWN,)
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
|
||||
@@ -34,8 +36,8 @@ SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA
|
||||
|
||||
vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -52,7 +54,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
entity_id = device_config[ATTR_ENTITY_ID]
|
||||
attribute = device_config.get(CONF_ATTRIBUTE)
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||
sensor_class = device_config[CONF_SENSOR_CLASS]
|
||||
device_class = get_deprecated(
|
||||
device_config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
invert = device_config[CONF_INVERT]
|
||||
|
||||
sensors.append(
|
||||
@@ -62,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
friendly_name,
|
||||
entity_id,
|
||||
attribute,
|
||||
sensor_class,
|
||||
device_class,
|
||||
invert)
|
||||
)
|
||||
if not sensors:
|
||||
@@ -76,7 +79,7 @@ class SensorTrend(BinarySensorDevice):
|
||||
"""Representation of a trend Sensor."""
|
||||
|
||||
def __init__(self, hass, device_id, friendly_name,
|
||||
target_entity, attribute, sensor_class, invert):
|
||||
target_entity, attribute, device_class, invert):
|
||||
"""Initialize the sensor."""
|
||||
self._hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
|
||||
@@ -84,7 +87,7 @@ class SensorTrend(BinarySensorDevice):
|
||||
self._name = friendly_name
|
||||
self._target_entity = target_entity
|
||||
self._attribute = attribute
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._invert = invert
|
||||
self._state = None
|
||||
self.from_state = None
|
||||
@@ -111,9 +114,9 @@ class SensorTrend(BinarySensorDevice):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice)
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@@ -30,6 +30,7 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._state = False
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Support for VOC.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.volvooncall/
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.volvooncall import VolvoEntity
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Volvo sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
add_devices([VolvoSensor(hass, *discovery_info)])
|
||||
|
||||
|
||||
class VolvoSensor(VolvoEntity, BinarySensorDevice):
|
||||
"""Representation of a Volvo sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
val = getattr(self.vehicle, self._attribute)
|
||||
if self._attribute == 'bulb_failures':
|
||||
return len(val) > 0
|
||||
elif self._attribute in ['doors', 'windows']:
|
||||
return any([val[key] for key in val if 'Open' in key])
|
||||
else:
|
||||
return val != 'Normal'
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return 'safety'
|
||||
@@ -92,13 +92,13 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
try:
|
||||
if hasattr(self.wink, 'unit'):
|
||||
self._unit_of_measurement = self.wink.unit()
|
||||
except AttributeError:
|
||||
else:
|
||||
self._unit_of_measurement = None
|
||||
try:
|
||||
if hasattr(self.wink, 'capability'):
|
||||
self.capability = self.wink.capability()
|
||||
except AttributeError:
|
||||
else:
|
||||
self.capability = None
|
||||
|
||||
@property
|
||||
@@ -107,8 +107,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
|
||||
@@ -161,8 +161,8 @@ class WinkRemote(WinkBinarySensorDevice):
|
||||
}
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Sensor to indicate whether the current day is a workday."""
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
CONF_NAME, WEEKDAYS)
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.8.1']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Canada', 'CA',
|
||||
'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'England',
|
||||
'EuropeanCentralBank', 'ECB', 'TAR', 'Germany', 'DE',
|
||||
'Ireland', 'Isle of Man', 'Mexico', 'MX', 'Netherlands', 'NL',
|
||||
'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO',
|
||||
'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Spain',
|
||||
'ES', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales']
|
||||
CONF_COUNTRY = 'country'
|
||||
CONF_PROVINCE = 'province'
|
||||
CONF_WORKDAYS = 'workdays'
|
||||
# By default, Monday - Friday are workdays
|
||||
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
|
||||
CONF_EXCLUDES = 'excludes'
|
||||
# By default, public holidays, Saturdays and Sundays are excluded from workdays
|
||||
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
|
||||
DEFAULT_NAME = 'Workday Sensor'
|
||||
ALLOWED_DAYS = WEEKDAYS + ['holiday']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
|
||||
vol.Optional(CONF_PROVINCE, default=None): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Workday sensor."""
|
||||
import holidays
|
||||
|
||||
# Get the Sensor name from the config
|
||||
sensor_name = config.get(CONF_NAME)
|
||||
|
||||
# Get the country code from the config
|
||||
country = config.get(CONF_COUNTRY)
|
||||
|
||||
# Get the province from the config
|
||||
province = config.get(CONF_PROVINCE)
|
||||
|
||||
# Get the list of workdays from the config
|
||||
workdays = config.get(CONF_WORKDAYS)
|
||||
|
||||
# Get the list of excludes from the config
|
||||
excludes = config.get(CONF_EXCLUDES)
|
||||
|
||||
# Instantiate the holidays module for the current year
|
||||
year = datetime.datetime.now().year
|
||||
obj_holidays = getattr(holidays, country)(years=year)
|
||||
|
||||
# Also apply the provience, if available for the configured country
|
||||
if province:
|
||||
if province not in obj_holidays.PROVINCES:
|
||||
_LOGGER.error('There is no province/state %s in country %s',
|
||||
province, country)
|
||||
return False
|
||||
else:
|
||||
year = datetime.datetime.now().year
|
||||
obj_holidays = getattr(holidays, country)(prov=province,
|
||||
years=year)
|
||||
|
||||
# Output found public holidays via the debug channel
|
||||
_LOGGER.debug("Found the following holidays for your configuration:")
|
||||
for date, name in sorted(obj_holidays.items()):
|
||||
_LOGGER.debug("%s %s", date, name)
|
||||
|
||||
# Add ourselves as device
|
||||
add_devices([IsWorkdaySensor(obj_holidays, workdays,
|
||||
excludes, sensor_name)], True)
|
||||
|
||||
|
||||
def day_to_string(day):
|
||||
"""Convert day index 0 - 7 to string."""
|
||||
try:
|
||||
return ALLOWED_DAYS[day]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
class IsWorkdaySensor(Entity):
|
||||
"""Implementation of a Workday sensor."""
|
||||
|
||||
def __init__(self, obj_holidays, workdays, excludes, name):
|
||||
"""Initialize the Workday sensor."""
|
||||
self._name = name
|
||||
self._obj_holidays = obj_holidays
|
||||
self._workdays = workdays
|
||||
self._excludes = excludes
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def is_include(self, day, now):
|
||||
"""Check if given day is in the includes list."""
|
||||
# Check includes
|
||||
if day in self._workdays:
|
||||
return True
|
||||
elif 'holiday' in self._workdays and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_exclude(self, day, now):
|
||||
"""Check if given day is in the excludes list."""
|
||||
# Check excludes
|
||||
if day in self._excludes:
|
||||
return True
|
||||
elif 'holiday' in self._excludes and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get date and look whether it is a holiday."""
|
||||
# Default is no workday
|
||||
self._state = STATE_OFF
|
||||
|
||||
# Get iso day of the week (1 = Monday, 7 = Sunday)
|
||||
day = datetime.datetime.today().isoweekday() - 1
|
||||
day_of_week = day_to_string(day)
|
||||
|
||||
if self.is_include(day_of_week, dt_util.now()):
|
||||
self._state = STATE_ON
|
||||
|
||||
if self.is_exclude(day_of_week, dt_util.now()):
|
||||
self._state = STATE_OFF
|
||||
@@ -24,7 +24,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the ZigBee binary sensor platform."""
|
||||
add_devices([ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))])
|
||||
add_devices(
|
||||
[ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))], True)
|
||||
|
||||
|
||||
class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice):
|
||||
|
||||
@@ -10,6 +10,7 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import workaround
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
BinarySensorDevice)
|
||||
@@ -18,88 +19,71 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Z-Wave platform for binary sensors."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
|
||||
device_mapping = workaround.get_device_mapping(value)
|
||||
def get_device(values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
device_mapping = workaround.get_device_mapping(values.primary)
|
||||
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
|
||||
# Default the multiplier to 4
|
||||
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
|
||||
add_devices([
|
||||
ZWaveTriggerSensor(value, "motion",
|
||||
hass, re_arm_multiplier * 8)
|
||||
])
|
||||
return
|
||||
re_arm_multiplier = zwave.get_config_value(values.primary.node, 9) or 4
|
||||
return ZWaveTriggerSensor(values, "motion", re_arm_multiplier * 8)
|
||||
|
||||
if workaround.get_device_component_mapping(value) == DOMAIN:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
return
|
||||
if workaround.get_device_component_mapping(values.primary) == DOMAIN:
|
||||
return ZWaveBinarySensor(values, None)
|
||||
|
||||
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
if values.primary.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
return ZWaveBinarySensor(values, None)
|
||||
return None
|
||||
|
||||
|
||||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
"""Representation of a binary sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, sensor_class):
|
||||
def __init__(self, values, device_class):
|
||||
"""Initialize the sensor."""
|
||||
self._sensor_type = sensor_class
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._sensor_type = device_class
|
||||
self._state = self.values.primary.data
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self.values.primary.data
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._value.data
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
"""Representation of a stateless sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, sensor_class, hass, re_arm_sec=60):
|
||||
def __init__(self, values, device_class, re_arm_sec=60):
|
||||
"""Initialize the sensor."""
|
||||
super(ZWaveTriggerSensor, self).__init__(value, sensor_class)
|
||||
self._hass = hass
|
||||
super(ZWaveTriggerSensor, self).__init__(values, device_class)
|
||||
self.re_arm_sec = re_arm_sec
|
||||
self.invalidate_after = None
|
||||
|
||||
def update_properties(self):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
self._state = self.values.primary.data
|
||||
# only allow this value to be true for re_arm secs
|
||||
if not self.hass:
|
||||
return
|
||||
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
# If it's active make sure that we set the timeout tracker
|
||||
if value.data:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.schedule_update_ha_state()
|
||||
if value.data:
|
||||
# only allow this value to be true for re_arm secs
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
track_point_in_time(
|
||||
self.hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if movement has happened within the rearm time."""
|
||||
return self._value.data and \
|
||||
return self._state and \
|
||||
(self.invalidate_after is None or
|
||||
self.invalidate_after > dt_util.utcnow())
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Support for Blink Home Camera System.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/blink/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_ARMED)
|
||||
from homeassistant.helpers import discovery
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'blink'
|
||||
REQUIREMENTS = ['blinkpy==0.5.2']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ARM_SYSTEM_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ARMED): cv.boolean
|
||||
})
|
||||
|
||||
ARM_CAMERA_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(ATTR_ARMED): cv.boolean
|
||||
})
|
||||
|
||||
SNAP_PICTURE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_FRIENDLY_NAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
class BlinkSystem(object):
|
||||
"""Blink System class."""
|
||||
|
||||
def __init__(self, config_info):
|
||||
"""Initialize the system."""
|
||||
import blinkpy
|
||||
self.blink = blinkpy.Blink(username=config_info[DOMAIN][CONF_USERNAME],
|
||||
password=config_info[DOMAIN][CONF_PASSWORD])
|
||||
self.blink.setup_system()
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup Blink System."""
|
||||
hass.data[DOMAIN] = BlinkSystem(config)
|
||||
discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
||||
|
||||
def snap_picture(call):
|
||||
"""Take a picture."""
|
||||
cameras = hass.data[DOMAIN].blink.cameras
|
||||
name = call.data.get(ATTR_FRIENDLY_NAME, '')
|
||||
if name in cameras:
|
||||
cameras[name].snap_picture()
|
||||
|
||||
def arm_camera(call):
|
||||
"""Arm a camera."""
|
||||
cameras = hass.data[DOMAIN].blink.cameras
|
||||
name = call.data.get(ATTR_FRIENDLY_NAME, '')
|
||||
value = call.data.get(ATTR_ARMED, True)
|
||||
if name in cameras:
|
||||
cameras[name].set_motion_detect(value)
|
||||
|
||||
def arm_system(call):
|
||||
"""Arm the system."""
|
||||
value = call.data.get(ATTR_ARMED, True)
|
||||
hass.data[DOMAIN].blink.arm = value
|
||||
hass.data[DOMAIN].blink.refresh()
|
||||
|
||||
hass.services.register(DOMAIN, 'snap_picture', snap_picture,
|
||||
schema=SNAP_PICTURE_SCHEMA)
|
||||
hass.services.register(DOMAIN, 'arm_camera', arm_camera,
|
||||
schema=ARM_CAMERA_SCHEMA)
|
||||
hass.services.register(DOMAIN, 'arm_system', arm_system,
|
||||
schema=ARM_SYSTEM_SCHEMA)
|
||||
|
||||
return True
|
||||
@@ -3,8 +3,8 @@ Support for Google Calendar event device sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -27,13 +27,13 @@ DOMAIN = 'calendar'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ class CalendarEventDevice(Entity):
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
||||
summary = self.data.event['summary']
|
||||
summary = self.data.event.get('summary', '')
|
||||
|
||||
# check if we have an offset tag in the message
|
||||
# time is HH:MM or MM
|
||||
|
||||
@@ -66,7 +66,7 @@ class GoogleCalendarData(object):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.start_of_local_day().isoformat('T')
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
if self.search:
|
||||
params['q'] = self.search
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Support for Blink system camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.blink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
|
||||
from homeassistant.components.blink import DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup a Blink Camera."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[DOMAIN].blink
|
||||
devs = list()
|
||||
for name in data.cameras:
|
||||
devs.append(BlinkCamera(hass, config, data, name))
|
||||
|
||||
add_devices(devs)
|
||||
|
||||
|
||||
class BlinkCamera(Camera):
|
||||
"""An implementation of a Blink Camera."""
|
||||
|
||||
def __init__(self, hass, config, data, name):
|
||||
"""Initialize a camera."""
|
||||
super().__init__()
|
||||
self.data = data
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.notifications = self.data.cameras[self._name].notifications
|
||||
self.response = None
|
||||
|
||||
_LOGGER.info("Initialized blink camera %s", self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""A camera name."""
|
||||
return self._name
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def request_image(self):
|
||||
"""An image request from Blink servers."""
|
||||
_LOGGER.info("Requesting new image from blink servers")
|
||||
image_url = self.check_for_motion()
|
||||
header = self.data.cameras[self._name].header
|
||||
self.response = requests.get(image_url, headers=header, stream=True)
|
||||
|
||||
def check_for_motion(self):
|
||||
"""A method to check if motion has been detected since last update."""
|
||||
self.data.refresh()
|
||||
notifs = self.data.cameras[self._name].notifications
|
||||
if notifs > self.notifications:
|
||||
# We detected motion at some point
|
||||
self.data.last_motion()
|
||||
self.notifications = notifs
|
||||
# returning motion image currently not working
|
||||
# return self.data.cameras[self._name].motion['image']
|
||||
elif notifs < self.notifications:
|
||||
self.notifications = notifs
|
||||
|
||||
return self.data.camera_thumbs[self._name]
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
self.request_image()
|
||||
return self.response.content
|
||||
@@ -34,7 +34,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
yield from async_add_devices([FFmpegCamera(hass, config)])
|
||||
async_add_devices([FFmpegCamera(hass, config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
|
||||
@@ -47,15 +47,21 @@ class FoscamCamera(Camera):
|
||||
port = device_info.get(CONF_PORT)
|
||||
|
||||
self._base_url = 'http://{}:{}/'.format(ip_address, port)
|
||||
|
||||
uri_template = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?' \
|
||||
+ 'cmd=snapPicture2&usr={}&pwd={}'
|
||||
|
||||
self._username = device_info.get(CONF_USERNAME)
|
||||
self._password = device_info.get(CONF_PASSWORD)
|
||||
self._snap_picture_url = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \
|
||||
+ self._username + '&pwd=' + self._password
|
||||
self._snap_picture_url = uri_template.format(
|
||||
self._username,
|
||||
self._password
|
||||
)
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
|
||||
_LOGGER.info('Using the following URL for %s: %s',
|
||||
self._name, self._snap_picture_url)
|
||||
self._name, uri_template.format('***', '***'))
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
|
||||
@@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a generic IP Camera."""
|
||||
yield from async_add_devices([GenericCamera(hass, config)])
|
||||
async_add_devices([GenericCamera(hass, config)])
|
||||
|
||||
|
||||
class GenericCamera(Camera):
|
||||
@@ -118,12 +118,13 @@ class GenericCamera(Camera):
|
||||
_LOGGER.error('Timeout getting camera image')
|
||||
return self._last_image
|
||||
except (aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError) as err:
|
||||
aiohttp.errors.DisconnectedError,
|
||||
aiohttp.errors.HttpProcessingError) as err:
|
||||
_LOGGER.error('Error getting new camera image: %s', err)
|
||||
return self._last_image
|
||||
finally:
|
||||
if response is not None:
|
||||
self.hass.async_add_job(response.release())
|
||||
yield from response.release()
|
||||
|
||||
self._last_url = url
|
||||
return self._last_image
|
||||
|
||||
@@ -20,7 +20,7 @@ CONF_FILE_PATH = 'file_path'
|
||||
DEFAULT_NAME = 'Local File'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_FILE_PATH): cv.isfile,
|
||||
vol.Required(CONF_FILE_PATH): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
@@ -31,8 +31,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# check filepath given is readable
|
||||
if not os.access(file_path, os.R_OK):
|
||||
_LOGGER.error("file path is not readable")
|
||||
return False
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
config[CONF_NAME], file_path)
|
||||
|
||||
add_devices([LocalFile(config[CONF_NAME], file_path)])
|
||||
|
||||
@@ -49,8 +49,12 @@ class LocalFile(Camera):
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
with open(self._file_path, 'rb') as file:
|
||||
return file.read()
|
||||
try:
|
||||
with open(self._file_path, 'rb') as file:
|
||||
return file.read()
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
self._name, self._file_path)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -45,7 +45,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a MJPEG IP Camera."""
|
||||
yield from async_add_devices([MjpegCamera(hass, config)])
|
||||
if discovery_info:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
async_add_devices([MjpegCamera(hass, config)])
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
|
||||
@@ -14,7 +14,7 @@ import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
@@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
TIMEOUT = 5
|
||||
DEFAULT_TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
|
||||
@@ -51,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
})
|
||||
@@ -60,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||
|
||||
# Determine API to use for authentication
|
||||
@@ -74,7 +76,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
}
|
||||
query_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
@@ -103,7 +105,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
websession_init,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url
|
||||
syno_auth_url,
|
||||
timeout
|
||||
)
|
||||
|
||||
# init websession
|
||||
@@ -120,7 +123,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
camera_req = yield from websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload
|
||||
@@ -149,15 +152,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_path
|
||||
auth_path,
|
||||
timeout
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, websession, username, password, login_url):
|
||||
def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||
"""Get a session id."""
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
@@ -170,7 +174,7 @@ def get_session_id(hass, websession, username, password, login_url):
|
||||
}
|
||||
auth_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
@@ -192,7 +196,7 @@ class SynologyCamera(Camera):
|
||||
|
||||
def __init__(self, hass, websession, config, camera_id,
|
||||
camera_name, snapshot_path, streaming_path, camera_path,
|
||||
auth_path):
|
||||
auth_path, timeout):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
@@ -206,6 +210,7 @@ class SynologyCamera(Camera):
|
||||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
self._timeout = timeout
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
@@ -225,7 +230,7 @@ class SynologyCamera(Camera):
|
||||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||
response = yield from self._websession.get(
|
||||
image_url,
|
||||
params=image_payload
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Support for ZoneMinder camera streaming.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.zoneminder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from urllib.parse import urljoin, urlencode
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
|
||||
import homeassistant.components.zoneminder as zoneminder
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['zoneminder']
|
||||
DOMAIN = 'zoneminder'
|
||||
|
||||
# From ZoneMinder's web/includes/config.php.in
|
||||
ZM_STATE_ALARM = "2"
|
||||
|
||||
|
||||
def _get_image_url(hass, monitor, mode):
|
||||
zm_data = hass.data[DOMAIN]
|
||||
query = urlencode({
|
||||
'mode': mode,
|
||||
'buffer': monitor['StreamReplayBuffer'],
|
||||
'monitor': monitor['Id'],
|
||||
})
|
||||
url = '{zms_url}?{query}'.format(
|
||||
zms_url=urljoin(zm_data['server_origin'], zm_data['path_zms']),
|
||||
query=query,
|
||||
)
|
||||
_LOGGER.debug('Monitor %s %s URL (without auth): %s',
|
||||
monitor['Id'], mode, url)
|
||||
|
||||
if not zm_data['username']:
|
||||
return url
|
||||
|
||||
url += '&user={:s}'.format(zm_data['username'])
|
||||
|
||||
if not zm_data['password']:
|
||||
return url
|
||||
|
||||
return url + '&pass={:s}'.format(zm_data['password'])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup ZoneMinder cameras."""
|
||||
cameras = []
|
||||
monitors = zoneminder.get_state('api/monitors.json')
|
||||
if not monitors:
|
||||
_LOGGER.warning('Could not fetch monitors from ZoneMinder')
|
||||
return
|
||||
|
||||
for i in monitors['monitors']:
|
||||
monitor = i['Monitor']
|
||||
|
||||
if monitor['Function'] == 'None':
|
||||
_LOGGER.info('Skipping camera %s', monitor['Id'])
|
||||
continue
|
||||
|
||||
_LOGGER.info('Initializing camera %s', monitor['Id'])
|
||||
|
||||
device_info = {
|
||||
CONF_NAME: monitor['Name'],
|
||||
CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
|
||||
}
|
||||
cameras.append(ZoneMinderCamera(hass, device_info, monitor))
|
||||
|
||||
if not cameras:
|
||||
_LOGGER.warning('No active cameras found')
|
||||
return
|
||||
|
||||
async_add_devices(cameras)
|
||||
|
||||
|
||||
class ZoneMinderCamera(MjpegCamera):
|
||||
"""Representation of a ZoneMinder Monitor Stream."""
|
||||
|
||||
def __init__(self, hass, device_info, monitor):
|
||||
"""Initialize as a subclass of MjpegCamera."""
|
||||
super().__init__(hass, device_info)
|
||||
self._monitor_id = int(monitor['Id'])
|
||||
self._is_recording = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Update the recording state periodically."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Update our recording state from the ZM API."""
|
||||
_LOGGER.debug('Updating camera state for monitor %i', self._monitor_id)
|
||||
status_response = zoneminder.get_state(
|
||||
'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
|
||||
)
|
||||
if not status_response:
|
||||
_LOGGER.warning('Could not get status for monitor %i',
|
||||
self._monitor_id)
|
||||
return
|
||||
|
||||
self._is_recording = status_response['status'] == ZM_STATE_ALARM
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return whether the monitor is in alarm mode."""
|
||||
return self._is_recording
|
||||
@@ -224,7 +224,7 @@ def async_setup(hass, config):
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
climate.async_update_ha_state(True))
|
||||
if hasattr(climate, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
@@ -25,6 +25,8 @@ ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time'
|
||||
ATTR_RESUME_ALL = 'resume_all'
|
||||
|
||||
DEFAULT_RESUME_ALL = False
|
||||
TEMPERATURE_HOLD = 'temp'
|
||||
VACATION_HOLD = 'vacation'
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
|
||||
@@ -112,6 +114,8 @@ class Thermostat(ClimateDevice):
|
||||
self.thermostat_index)
|
||||
self._name = self.thermostat['name']
|
||||
self.hold_temp = hold_temp
|
||||
self.vacation = None
|
||||
self._climate_list = self.climate_list
|
||||
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
|
||||
'heat', 'off']
|
||||
self.update_without_throttle = False
|
||||
@@ -187,29 +191,30 @@ class Thermostat(ClimateDevice):
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
events = self.thermostat['events']
|
||||
if any((event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) <= 1)
|
||||
or event['type'] == 'autoAway'
|
||||
for event in events):
|
||||
# away hold is auto away or a temporary hold from away climate
|
||||
hold = 'away'
|
||||
elif any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events):
|
||||
# a permanent away is not considered a hold, but away_mode
|
||||
hold = None
|
||||
elif any(event['holdClimateRef'] == 'home' or
|
||||
event['type'] == 'autoHome'
|
||||
for event in events):
|
||||
# home mode is auto home or any home hold
|
||||
hold = 'home'
|
||||
elif any(event['type'] == 'hold' and event['running']
|
||||
for event in events):
|
||||
hold = 'temp'
|
||||
# temperature hold is any other hold not based on climate
|
||||
else:
|
||||
hold = None
|
||||
return hold
|
||||
for event in events:
|
||||
if event['running']:
|
||||
if event['type'] == 'hold':
|
||||
if event['holdClimateRef'] == 'away':
|
||||
if int(event['endDate'][0:4]) - \
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# a temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
else:
|
||||
# a premanent hold from away climate is away_mode
|
||||
return None
|
||||
elif event['holdClimateRef'] != "":
|
||||
# any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
else:
|
||||
# any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
elif event['type'].startswith('auto'):
|
||||
# all auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
elif event['type'] == 'vacation':
|
||||
self.vacation = event['name']
|
||||
return VACATION_HOLD
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
@@ -232,8 +237,11 @@ class Thermostat(ClimateDevice):
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
"""Return current mode ie. home, away, sleep."""
|
||||
return self.thermostat['program']['currentClimateRef']
|
||||
"""Return current mode, as the user-visible name."""
|
||||
cur = self.thermostat['program']['currentClimateRef']
|
||||
climates = self.thermostat['program']['climates']
|
||||
current = list(filter(lambda x: x['climateRef'] == cur, climates))
|
||||
return current[0]['name']
|
||||
|
||||
@property
|
||||
def fan_min_on_time(self):
|
||||
@@ -261,52 +269,44 @@ class Thermostat(ClimateDevice):
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"operation": operation,
|
||||
"climate_list": self.climate_list,
|
||||
"fan_min_on_time": self.fan_min_on_time
|
||||
}
|
||||
|
||||
def is_vacation_on(self):
|
||||
"""Return true if vacation mode is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['type'] == 'vacation' and event['running']
|
||||
for event in events)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events)
|
||||
return self.current_hold_mode == 'away'
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", 'indefinite')
|
||||
self.update_without_throttle = True
|
||||
self.set_hold_mode('away')
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
self.set_hold_mode(None)
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Set hold mode (away, home, temp)."""
|
||||
"""Set hold mode (away, home, temp, sleep, etc.)."""
|
||||
hold = self.current_hold_mode
|
||||
|
||||
if hold == hold_mode:
|
||||
# no change, so no action required
|
||||
return
|
||||
elif hold_mode == 'away':
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", self.hold_preference())
|
||||
elif hold_mode == 'home':
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"home", self.hold_preference())
|
||||
elif hold_mode == 'temp':
|
||||
self.set_temp_hold(int(self.current_temperature))
|
||||
elif hold_mode == 'None' or hold_mode is None:
|
||||
if hold == VACATION_HOLD:
|
||||
self.data.ecobee.delete_vacation(self.thermostat_index,
|
||||
self.vacation)
|
||||
else:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
else:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
if hold_mode == TEMPERATURE_HOLD:
|
||||
self.set_temp_hold(int(self.current_temperature))
|
||||
else:
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
hold_mode,
|
||||
self.hold_preference())
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_auto_temp_hold(self, heat_temp, cool_temp):
|
||||
"""Set temperature hold in auto mode."""
|
||||
@@ -382,3 +382,9 @@ class Thermostat(ClimateDevice):
|
||||
# as an indefinite away hold is interpreted as away_mode
|
||||
else:
|
||||
return 'nextTransition'
|
||||
|
||||
@property
|
||||
def climate_list(self):
|
||||
"""Return the list of climates currently available."""
|
||||
climates = self.thermostat['program']['climates']
|
||||
return list(map((lambda x: x['name']), climates))
|
||||
|
||||
@@ -16,7 +16,8 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_time_interval)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,6 +36,7 @@ CONF_TARGET_TEMP = 'target_temp'
|
||||
CONF_AC_MODE = 'ac_mode'
|
||||
CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_TOLERANCE = 'tolerance'
|
||||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -47,6 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_KEEP_ALIVE): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
})
|
||||
|
||||
|
||||
@@ -62,10 +66,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
ac_mode = config.get(CONF_AC_MODE)
|
||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
tolerance = config.get(CONF_TOLERANCE)
|
||||
keep_alive = config.get(CONF_KEEP_ALIVE)
|
||||
|
||||
yield from async_add_devices([GenericThermostat(
|
||||
async_add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance)])
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance, keep_alive)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
@@ -73,7 +78,7 @@ 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,
|
||||
tolerance):
|
||||
tolerance, keep_alive):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@@ -81,6 +86,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self.ac_mode = ac_mode
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
self._keep_alive = keep_alive
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@@ -94,6 +100,10 @@ class GenericThermostat(ClimateDevice):
|
||||
async_track_state_change(
|
||||
hass, heater_entity_id, self._async_switch_changed)
|
||||
|
||||
if self._keep_alive:
|
||||
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)
|
||||
@@ -180,6 +190,14 @@ class GenericThermostat(ClimateDevice):
|
||||
return
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@callback
|
||||
def _async_keep_alive(self, time):
|
||||
"""Called at constant intervals for keep-alive purposes."""
|
||||
if self.current_operation in [STATE_COOL, STATE_HEAT]:
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
|
||||
@callback
|
||||
def _async_update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
|
||||
@@ -6,13 +6,14 @@ https://home-assistant.io/components/climate.homematic/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.util.temperature import convert
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_MANUAL = "manual"
|
||||
STATE_BOOST = "boost"
|
||||
|
||||
@@ -22,21 +23,31 @@ HM_STATE_MAP = {
|
||||
"BOOST_MODE": STATE_BOOST,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
HM_TEMP_MAP = [
|
||||
'ACTUAL_TEMPERATURE',
|
||||
'TEMPERATURE',
|
||||
]
|
||||
|
||||
HM_HUMI_MAP = [
|
||||
'ACTUAL_HUMIDITY',
|
||||
'HUMIDITY',
|
||||
]
|
||||
|
||||
HM_CONTROL_MODE = 'CONTROL_MODE'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Homematic thermostat platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMThermostat,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, config)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class HMThermostat(HMDevice, ClimateDevice):
|
||||
@@ -50,7 +61,7 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self.available:
|
||||
if HM_CONTROL_MODE not in self._data:
|
||||
return None
|
||||
|
||||
# read state and search
|
||||
@@ -62,8 +73,6 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
if not self.available:
|
||||
return None
|
||||
op_list = []
|
||||
|
||||
# generate list
|
||||
@@ -76,31 +85,29 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
if not self.available:
|
||||
return None
|
||||
return self._data.get('ACTUAL_HUMIDITY', None)
|
||||
for node in HM_HUMI_MAP:
|
||||
if node in self._data:
|
||||
return self._data[node]
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if not self.available:
|
||||
return None
|
||||
return self._data.get('ACTUAL_TEMPERATURE', None)
|
||||
for node in HM_TEMP_MAP:
|
||||
if node in self._data:
|
||||
return self._data[node]
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the target temperature."""
|
||||
if not self.available:
|
||||
return None
|
||||
return self._data.get('SET_TEMPERATURE', None)
|
||||
return self._data.get(self._state)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if not self.available or temperature is None:
|
||||
if temperature is None:
|
||||
return None
|
||||
|
||||
self._hmdevice.set_temperature(temperature)
|
||||
self._hmdevice.writeNodeData(self._state, float(temperature))
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
@@ -122,10 +129,12 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
# Add state to data dict
|
||||
self._data.update({"CONTROL_MODE": STATE_UNKNOWN,
|
||||
"SET_TEMPERATURE": STATE_UNKNOWN,
|
||||
"ACTUAL_TEMPERATURE": STATE_UNKNOWN})
|
||||
self._state = next(iter(self._hmdevice.WRITENODE.keys()))
|
||||
self._data[self._state] = STATE_UNKNOWN
|
||||
|
||||
# support humidity
|
||||
if 'ACTUAL_HUMIDITY' in self._hmdevice.SENSORNODE:
|
||||
self._data.update({'ACTUAL_HUMIDITY': STATE_UNKNOWN})
|
||||
# support state
|
||||
if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data[HM_CONTROL_MODE] = STATE_UNKNOWN
|
||||
|
||||
for node in self._hmdevice.SENSORNODE.keys():
|
||||
self._data[node] = STATE_UNKNOWN
|
||||
|
||||
@@ -6,10 +6,15 @@ https://home-assistant.io/components/climate.honeywell/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA,
|
||||
ATTR_FAN_MODE, ATTR_FAN_LIST,
|
||||
ATTR_OPERATION_MODE,
|
||||
ATTR_OPERATION_LIST)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE)
|
||||
@@ -21,27 +26,35 @@ REQUIREMENTS = ['evohomeclient==0.2.5',
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
ATTR_FANMODE = 'fanmode'
|
||||
ATTR_SYSTEM_MODE = 'system_mode'
|
||||
ATTR_CURRENT_OPERATION = 'equipment_output_status'
|
||||
|
||||
CONF_AWAY_TEMPERATURE = 'away_temperature'
|
||||
CONF_COOL_AWAY_TEMPERATURE = 'away_cool_temperature'
|
||||
CONF_HEAT_AWAY_TEMPERATURE = 'away_heat_temperature'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE = 16
|
||||
DEFAULT_COOL_AWAY_TEMPERATURE = 30
|
||||
DEFAULT_HEAT_AWAY_TEMPERATURE = 16
|
||||
DEFAULT_REGION = 'eu'
|
||||
REGIONS = ['eu', 'us']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE, default=DEFAULT_AWAY_TEMPERATURE):
|
||||
vol.Coerce(float),
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE,
|
||||
default=DEFAULT_AWAY_TEMPERATURE): vol.Coerce(float),
|
||||
vol.Optional(CONF_COOL_AWAY_TEMPERATURE,
|
||||
default=DEFAULT_COOL_AWAY_TEMPERATURE): vol.Coerce(float),
|
||||
vol.Optional(CONF_HEAT_AWAY_TEMPERATURE,
|
||||
default=DEFAULT_HEAT_AWAY_TEMPERATURE): vol.Coerce(float),
|
||||
vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the HoneywelL thermostat."""
|
||||
"""Setup the Honeywell thermostat."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
region = config.get(CONF_REGION)
|
||||
@@ -88,8 +101,11 @@ def _setup_us(username, password, config, add_devices):
|
||||
|
||||
dev_id = config.get('thermostat')
|
||||
loc_id = config.get('location')
|
||||
cool_away_temp = config.get(CONF_COOL_AWAY_TEMPERATURE)
|
||||
heat_away_temp = config.get(CONF_HEAT_AWAY_TEMPERATURE)
|
||||
|
||||
add_devices([HoneywellUSThermostat(client, device)
|
||||
add_devices([HoneywellUSThermostat(client, device, cool_away_temp,
|
||||
heat_away_temp, username, password)
|
||||
for location in client.locations_by_id.values()
|
||||
for device in location.devices_by_id.values()
|
||||
if ((not loc_id or location.locationid == loc_id) and
|
||||
@@ -160,7 +176,7 @@ class RoundThermostat(ClimateDevice):
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on.
|
||||
|
||||
Evohome does have a proprietary away mode, but it doesn't really work
|
||||
Honeywell does have a proprietary away mode, but it doesn't really work
|
||||
the way it should. For example: If you set a temperature manually
|
||||
it doesn't get overwritten when away mode is switched on.
|
||||
"""
|
||||
@@ -199,10 +215,16 @@ class RoundThermostat(ClimateDevice):
|
||||
class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Representation of a Honeywell US Thermostat."""
|
||||
|
||||
def __init__(self, client, device):
|
||||
def __init__(self, client, device, cool_away_temp,
|
||||
heat_away_temp, username, password):
|
||||
"""Initialize the thermostat."""
|
||||
self._client = client
|
||||
self._device = device
|
||||
self._cool_away_temp = cool_away_temp
|
||||
self._heat_away_temp = heat_away_temp
|
||||
self._away = False
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
@property
|
||||
def is_fan_on(self):
|
||||
@@ -236,7 +258,10 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return getattr(self._device, ATTR_SYSTEM_MODE, None)
|
||||
oper = getattr(self._device, ATTR_CURRENT_OPERATION, None)
|
||||
if oper == "off":
|
||||
oper = "idle"
|
||||
return oper
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set target temperature."""
|
||||
@@ -245,29 +270,84 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
return
|
||||
import somecomfort
|
||||
try:
|
||||
if self._device.system_mode == 'cool':
|
||||
self._device.setpoint_cool = temperature
|
||||
else:
|
||||
self._device.setpoint_heat = temperature
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
# Set hold if this is not the case
|
||||
if getattr(self._device, "hold_{}".format(mode)) is False:
|
||||
# Get next period key
|
||||
next_period_key = '{}NextPeriod'.format(mode.capitalize())
|
||||
# Get next period raw value
|
||||
next_period = self._device.raw_ui_data.get(next_period_key)
|
||||
# Get next period time
|
||||
hour, minute = divmod(next_period * 15, 60)
|
||||
# Set hold time
|
||||
setattr(self._device,
|
||||
"hold_{}".format(mode),
|
||||
datetime.time(hour, minute))
|
||||
# Set temperature
|
||||
setattr(self._device,
|
||||
"setpoint_{}".format(mode),
|
||||
temperature)
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Temperature %.1f out of range', temperature)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
import somecomfort
|
||||
data = {
|
||||
ATTR_FAN: (self.is_fan_on and 'running' or 'idle'),
|
||||
ATTR_FANMODE: self._device.fan_mode,
|
||||
ATTR_SYSTEM_MODE: self._device.system_mode,
|
||||
ATTR_FAN_MODE: self._device.fan_mode,
|
||||
ATTR_OPERATION_MODE: self._device.system_mode,
|
||||
}
|
||||
data[ATTR_FAN_LIST] = somecomfort.FAN_MODES
|
||||
data[ATTR_OPERATION_LIST] = somecomfort.SYSTEM_MODES
|
||||
return data
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
pass
|
||||
"""Turn away on.
|
||||
|
||||
Somecomfort does have a proprietary away mode, but it doesn't really
|
||||
work the way it should. For example: If you set a temperature manually
|
||||
it doesn't get overwritten when away mode is switched on.
|
||||
"""
|
||||
self._away = True
|
||||
import somecomfort
|
||||
try:
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Can not get system mode')
|
||||
return
|
||||
try:
|
||||
|
||||
# Set permanent hold
|
||||
setattr(self._device,
|
||||
"hold_{}".format(mode),
|
||||
True)
|
||||
# Set temperature
|
||||
setattr(self._device,
|
||||
"setpoint_{}".format(mode),
|
||||
getattr(self, "_{}_away_temp".format(mode)))
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Temperature %.1f out of range',
|
||||
getattr(self, "_{}_away_temp".format(mode)))
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
pass
|
||||
self._away = False
|
||||
import somecomfort
|
||||
try:
|
||||
# Disabling all hold modes
|
||||
self._device.hold_cool = False
|
||||
self._device.hold_heat = False
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Can not stop hold mode')
|
||||
|
||||
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
|
||||
"""Set the system mode (Cool, Heat, etc)."""
|
||||
@@ -276,4 +356,49 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
self._device.refresh()
|
||||
import somecomfort
|
||||
retries = 3
|
||||
while retries > 0:
|
||||
try:
|
||||
self._device.refresh()
|
||||
break
|
||||
except (somecomfort.client.APIRateLimited, OSError,
|
||||
requests.exceptions.ReadTimeout) as exp:
|
||||
retries -= 1
|
||||
if retries == 0:
|
||||
raise exp
|
||||
if not self._retry():
|
||||
raise exp
|
||||
_LOGGER.error("SomeComfort update failed, Retrying "
|
||||
"- Error: %s", exp)
|
||||
|
||||
def _retry(self):
|
||||
"""Recreate a new somecomfort client.
|
||||
|
||||
When we got an error, the best way to be sure that the next query
|
||||
will succeed, is to recreate a new somecomfort client.
|
||||
"""
|
||||
import somecomfort
|
||||
try:
|
||||
self._client = somecomfort.SomeComfort(self._username,
|
||||
self._password)
|
||||
except somecomfort.AuthError:
|
||||
_LOGGER.error('Failed to login to honeywell account %s',
|
||||
self._username)
|
||||
return False
|
||||
except somecomfort.SomeComfortError as ex:
|
||||
_LOGGER.error('Failed to initialize honeywell client: %s',
|
||||
str(ex))
|
||||
return False
|
||||
|
||||
devices = [device
|
||||
for location in self._client.locations_by_id.values()
|
||||
for device in location.devices_by_id.values()
|
||||
if device.name == self._device.name]
|
||||
|
||||
if len(devices) != 1:
|
||||
_LOGGER.error('Failed to find device %s', self._device.name)
|
||||
return False
|
||||
|
||||
self._device = devices[0]
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Support for MAX! Thermostats via MAX! Cube.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/maxcube/
|
||||
"""
|
||||
|
||||
import socket
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_MANUAL = "manual"
|
||||
STATE_BOOST = "boost"
|
||||
STATE_VACATION = "vacation"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add thermostats to HASS."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
|
||||
# List of devices
|
||||
devices = []
|
||||
|
||||
for device in cube.devices:
|
||||
# Create device name by concatenating room name + device name
|
||||
name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
# Only add thermostats and wallthermostats
|
||||
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
|
||||
# Add device to HASS
|
||||
devices.append(MaxCubeClimate(hass, name, device.rf_address))
|
||||
|
||||
# Add all devices at once
|
||||
if len(devices) > 0:
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class MaxCubeClimate(ClimateDevice):
|
||||
"""MAX! Cube ClimateDevice."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
"""Initialize MAX! Cube ClimateDevice."""
|
||||
self._name = name
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
|
||||
STATE_VACATION]
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is required."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the ClimateDevice."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return minimum temperature
|
||||
return self.map_temperature_max_hass(device.min_temperature)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return maximum temperature
|
||||
return self.map_temperature_max_hass(device.max_temperature)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return current temperature
|
||||
return self.map_temperature_max_hass(device.actual_temperature)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation (auto, manual, boost, vacation)."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Mode Mapping
|
||||
return self.map_mode_max_hass(device.mode)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return target temperature
|
||||
return self.map_temperature_max_hass(device.target_temperature)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
# Fail is target temperature has not been supplied as argument
|
||||
if kwargs.get(ATTR_TEMPERATURE) is None:
|
||||
return False
|
||||
|
||||
# Determine the new target temperature
|
||||
target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
# Write the target temperature to the MAX! Cube.
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
cube = self._cubehandle.cube
|
||||
|
||||
with self._cubehandle.mutex:
|
||||
try:
|
||||
cube.set_target_temperature(device, target_temperature)
|
||||
except (socket.timeout, socket.error):
|
||||
_LOGGER.error("Setting target temperature failed")
|
||||
return False
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new operation mode."""
|
||||
# Get the device we want to update
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Mode Mapping
|
||||
mode = self.map_mode_hass_max(operation_mode)
|
||||
|
||||
# Write new mode to thermostat
|
||||
if mode is None:
|
||||
return False
|
||||
|
||||
with self._cubehandle.mutex:
|
||||
try:
|
||||
self._cubehandle.cube.set_mode(device, mode)
|
||||
except (socket.timeout, socket.error):
|
||||
_LOGGER.error("Setting operation mode failed")
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Get latest data from MAX! Cube."""
|
||||
# Update the CubeHandle
|
||||
self._cubehandle.update()
|
||||
|
||||
@staticmethod
|
||||
def map_temperature_max_hass(temperature):
|
||||
"""Map Temperature from MAX! to HASS."""
|
||||
if temperature is None:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
return temperature
|
||||
|
||||
@staticmethod
|
||||
def map_mode_hass_max(operation_mode):
|
||||
"""Map HASS Operation Modes to MAX! Operation Modes."""
|
||||
from maxcube.device import \
|
||||
MAX_DEVICE_MODE_AUTOMATIC, \
|
||||
MAX_DEVICE_MODE_MANUAL, \
|
||||
MAX_DEVICE_MODE_VACATION, \
|
||||
MAX_DEVICE_MODE_BOOST
|
||||
|
||||
if operation_mode == STATE_AUTO:
|
||||
mode = MAX_DEVICE_MODE_AUTOMATIC
|
||||
elif operation_mode == STATE_MANUAL:
|
||||
mode = MAX_DEVICE_MODE_MANUAL
|
||||
elif operation_mode == STATE_VACATION:
|
||||
mode = MAX_DEVICE_MODE_VACATION
|
||||
elif operation_mode == STATE_BOOST:
|
||||
mode = MAX_DEVICE_MODE_BOOST
|
||||
else:
|
||||
mode = None
|
||||
|
||||
return mode
|
||||
|
||||
@staticmethod
|
||||
def map_mode_max_hass(mode):
|
||||
"""Map MAX! Operation Modes to HASS Operation Modes."""
|
||||
from maxcube.device import \
|
||||
MAX_DEVICE_MODE_AUTOMATIC, \
|
||||
MAX_DEVICE_MODE_MANUAL, \
|
||||
MAX_DEVICE_MODE_VACATION, \
|
||||
MAX_DEVICE_MODE_BOOST
|
||||
|
||||
if mode == MAX_DEVICE_MODE_AUTOMATIC:
|
||||
operation_mode = STATE_AUTO
|
||||
elif mode == MAX_DEVICE_MODE_MANUAL:
|
||||
operation_mode = STATE_MANUAL
|
||||
elif mode == MAX_DEVICE_MODE_VACATION:
|
||||
operation_mode = STATE_VACATION
|
||||
elif mode == MAX_DEVICE_MODE_BOOST:
|
||||
operation_mode = STATE_BOOST
|
||||
else:
|
||||
operation_mode = None
|
||||
|
||||
return operation_mode
|
||||
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
OpenEnergyMonitor Thermostat Support.
|
||||
|
||||
This provides a climate component for the ESP8266 based thermostat sold by
|
||||
OpenEnergyMonitor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.oem/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
# Import the device class from the component that you want to support
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE)
|
||||
from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_PORT, TEMP_CELSIUS, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
# Home Assistant depends on 3rd party packages for API specific code.
|
||||
REQUIREMENTS = ['oemthermostat==1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Local configs
|
||||
CONF_AWAY_TEMP = 'away_temp'
|
||||
|
||||
# Validation of the user's configuration
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default="Thermostat"): cv.string,
|
||||
vol.Optional(CONF_PORT, default=80): cv.port,
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_AWAY_TEMP, default=14): vol.Coerce(float)
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup oemthermostat."""
|
||||
from oemthermostat import Thermostat
|
||||
|
||||
# Assign configuration variables. The configuration check takes care they
|
||||
# are present.
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
away_temp = config.get(CONF_AWAY_TEMP)
|
||||
|
||||
# If creating the class raises an exception, it failed to connect or
|
||||
# something else went wrong.
|
||||
try:
|
||||
therm = Thermostat(host, port=port,
|
||||
username=username, password=password)
|
||||
except (ValueError, AssertionError, requests.RequestException):
|
||||
return False
|
||||
|
||||
# Add devices
|
||||
add_devices((ThermostatDevice(hass, therm, name, away_temp), ), True)
|
||||
|
||||
|
||||
class ThermostatDevice(ClimateDevice):
|
||||
"""Interface class for the oemthermostat module and HA."""
|
||||
|
||||
def __init__(self, hass, thermostat, name, away_temp):
|
||||
"""Initialize the device."""
|
||||
self._name = name
|
||||
self.hass = hass
|
||||
|
||||
# Away mode stuff
|
||||
self._away = False
|
||||
self._away_temp = away_temp
|
||||
self._prev_temp = thermostat.setpoint
|
||||
|
||||
self.thermostat = thermostat
|
||||
# Set the thermostat mode to manual
|
||||
self.thermostat.mode = 2
|
||||
|
||||
# set up internal state varS
|
||||
self._state = None
|
||||
self._temperature = None
|
||||
self._setpoint = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of this Thermostat."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""The unit of measurement used by the platform."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation i.e. heat, cool, idle."""
|
||||
if self._state:
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._setpoint
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Change the setpoint of the thermostat."""
|
||||
# If we are setting the temp, then we don't want away mode anymore.
|
||||
self.turn_away_mode_off()
|
||||
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.thermostat.setpoint = temp
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
if not self._away:
|
||||
self._prev_temp = self._setpoint
|
||||
|
||||
self.thermostat.setpoint = self._away_temp
|
||||
self._away = True
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
if self._away:
|
||||
self.thermostat.setpoint = self._prev_temp
|
||||
|
||||
self._away = False
|
||||
|
||||
def update(self):
|
||||
"""Update local state."""
|
||||
self._setpoint = self.thermostat.setpoint
|
||||
self._temperature = self.thermostat.temperature
|
||||
self._state = self.thermostat.state
|
||||
@@ -0,0 +1,296 @@
|
||||
"""tado component to create a climate device for each zone."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE)
|
||||
from homeassistant.components.tado import (
|
||||
DATA_TADO)
|
||||
|
||||
CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode
|
||||
CONST_MODE_OFF = "OFF" # Switch off heating in a zone
|
||||
|
||||
# When we change the temperature setting, we need an overlay mode
|
||||
# wait until tado changes the mode automatic
|
||||
CONST_OVERLAY_TADO_MODE = "TADO_MODE"
|
||||
# the user has change the temperature or mode manually
|
||||
CONST_OVERLAY_MANUAL = "MANUAL"
|
||||
# the temperature will be reset after a timespan
|
||||
CONST_OVERLAY_TIMER = "TIMER"
|
||||
|
||||
OPERATION_LIST = {
|
||||
CONST_OVERLAY_MANUAL: "Manual",
|
||||
CONST_OVERLAY_TIMER: "Timer",
|
||||
CONST_OVERLAY_TADO_MODE: "Tado mode",
|
||||
CONST_MODE_SMART_SCHEDULE: "Smart schedule",
|
||||
CONST_MODE_OFF: "Off"}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the climate platform."""
|
||||
# get the PyTado object from the hub component
|
||||
tado = hass.data[DATA_TADO]
|
||||
|
||||
try:
|
||||
zones = tado.get_zones()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to get zone info from mytado")
|
||||
return False
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
climate_devices.append(create_climate_device(tado, hass,
|
||||
zone,
|
||||
zone['name'],
|
||||
zone['id']))
|
||||
|
||||
if len(climate_devices) > 0:
|
||||
add_devices(climate_devices, True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
"""Create a climate device."""
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
min_temp = float(capabilities["temperatures"]["celsius"]["min"])
|
||||
max_temp = float(capabilities["temperatures"]["celsius"]["max"])
|
||||
ac_mode = capabilities["type"] != "HEATING"
|
||||
|
||||
data_id = 'zone {} {}'.format(name, zone_id)
|
||||
device = TadoClimate(tado,
|
||||
name, zone_id, data_id,
|
||||
hass.config.units.temperature(min_temp, unit),
|
||||
hass.config.units.temperature(max_temp, unit),
|
||||
ac_mode)
|
||||
|
||||
tado.add_sensor(data_id, {
|
||||
"id": zone_id,
|
||||
"zone": zone,
|
||||
"name": name,
|
||||
"climate": device
|
||||
})
|
||||
|
||||
return device
|
||||
|
||||
|
||||
class TadoClimate(ClimateDevice):
|
||||
"""Representation of a tado climate device."""
|
||||
|
||||
def __init__(self, store, zone_name, zone_id, data_id,
|
||||
min_temp, max_temp, ac_mode,
|
||||
tolerance=0.3):
|
||||
"""Initialization of TadoClimate device."""
|
||||
self._store = store
|
||||
self._data_id = data_id
|
||||
|
||||
self.zone_name = zone_name
|
||||
self.zone_id = zone_id
|
||||
|
||||
self.ac_mode = ac_mode
|
||||
|
||||
self._active = False
|
||||
self._device_is_active = False
|
||||
|
||||
self._unit = TEMP_CELSIUS
|
||||
self._cur_temp = None
|
||||
self._cur_humidity = None
|
||||
self._is_away = False
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = None
|
||||
self._tolerance = tolerance
|
||||
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self.zone_name
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._cur_humidity
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the sensor temperature."""
|
||||
return self._cur_temp
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current readable operation mode."""
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes (readable)."""
|
||||
return list(OPERATION_LIST.values())
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""The unit of measurement used by the platform."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._is_away
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
self._current_operation = CONST_OVERLAY_TADO_MODE
|
||||
self._overlay_mode = None
|
||||
self._target_temp = temperature
|
||||
self._control_heating()
|
||||
|
||||
def set_operation_mode(self, readable_operation_mode):
|
||||
"""Set new operation mode."""
|
||||
operation_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
for mode, readable in OPERATION_LIST.items():
|
||||
if readable == readable_operation_mode:
|
||||
operation_mode = mode
|
||||
break
|
||||
|
||||
self._current_operation = operation_mode
|
||||
self._overlay_mode = None
|
||||
self._control_heating()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
self._store.update()
|
||||
|
||||
data = self._store.get_data(self._data_id)
|
||||
|
||||
if data is None:
|
||||
_LOGGER.debug('Recieved no data for zone %s',
|
||||
self.zone_name)
|
||||
return
|
||||
|
||||
if 'sensorDataPoints' in data:
|
||||
sensor_data = data['sensorDataPoints']
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
humidity = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
setting = 0
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if 'temperature' in data['setting'] and \
|
||||
data['setting']['temperature'] is not None:
|
||||
setting = float(
|
||||
data['setting']['temperature']['celsius'])
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
self._cur_temp = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
self._cur_humidity = humidity
|
||||
|
||||
if 'tadoMode' in data:
|
||||
mode = data['tadoMode']
|
||||
self._is_away = mode == "AWAY"
|
||||
|
||||
if 'setting' in data:
|
||||
power = data['setting']['power']
|
||||
if power == "OFF":
|
||||
self._current_operation = CONST_MODE_OFF
|
||||
self._device_is_active = False
|
||||
else:
|
||||
self._device_is_active = True
|
||||
|
||||
if 'overlay' in data and data['overlay'] is not None:
|
||||
overlay = True
|
||||
termination = data['overlay']['termination']['type']
|
||||
else:
|
||||
overlay = False
|
||||
termination = ""
|
||||
|
||||
# if you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
|
||||
if overlay and self._device_is_active:
|
||||
# there is an overlay the device is on
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
else:
|
||||
# there is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
def _control_heating(self):
|
||||
"""Send new target temperature to mytado."""
|
||||
if not self._active and None not in (self._cur_temp,
|
||||
self._target_temp):
|
||||
self._active = True
|
||||
_LOGGER.info('Obtained current and target temperature. '
|
||||
'tado thermostat active.')
|
||||
|
||||
if not self._active or self._current_operation == self._overlay_mode:
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
||||
_LOGGER.info('Switching mytado.com to SCHEDULE (default) '
|
||||
'for zone %s', self.zone_name)
|
||||
self._store.reset_zone_overlay(self.zone_id)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_OFF:
|
||||
_LOGGER.info('Switching mytado.com to OFF for zone %s',
|
||||
self.zone_name)
|
||||
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
_LOGGER.info('Switching mytado.com to %s mode for zone %s',
|
||||
self._current_operation, self.zone_name)
|
||||
self._store.set_zone_overlay(self.zone_id,
|
||||
self._current_operation,
|
||||
self._target_temp)
|
||||
|
||||
self._overlay_mode = self._current_operation
|
||||
@@ -7,14 +7,14 @@ https://home-assistant.io/components/switch.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE)
|
||||
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@@ -37,6 +37,7 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
@@ -84,11 +85,11 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
return self.vera_device.fan_cycle()
|
||||
|
||||
@property
|
||||
def current_power_mwh(self):
|
||||
"""Current power usage in mWh."""
|
||||
def current_power_w(self):
|
||||
"""Current power usage in W."""
|
||||
power = self.vera_device.power
|
||||
if power:
|
||||
return convert(power, float, 0.0) * 1000
|
||||
return convert(power, float, 0.0)
|
||||
|
||||
def update(self):
|
||||
"""Called by the vera device callback to update state."""
|
||||
|
||||
@@ -366,8 +366,8 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
if target_temp_low is not None:
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
data["total_consumption"] = self.wink.toatl_consumption()
|
||||
data["schedule_enabled"] = self.wink.toatl_consumption()
|
||||
data["total_consumption"] = self.wink.total_consumption()
|
||||
data["schedule_enabled"] = self.wink.schedule_enabled()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
|
||||
@@ -32,29 +32,18 @@ DEVICE_MAPPINGS = {
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Z-Wave Climate devices."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return
|
||||
def get_device(hass, values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZWaveClimate(value, temp_unit)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return ZWaveClimate(values, temp_unit)
|
||||
|
||||
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
def __init__(self, value, temp_unit):
|
||||
def __init__(self, values, temp_unit):
|
||||
"""Initialize the Z-Wave climate device."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._index = value.index
|
||||
self._node = value.node
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_operation = None
|
||||
@@ -68,101 +57,74 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._unit = temp_unit
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
self.update_properties()
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16))
|
||||
if (self.node.manufacturer_id.strip() and
|
||||
self.node.product_id.strip()):
|
||||
specific_sensor_key = (
|
||||
int(self.node.manufacturer_id, 16),
|
||||
int(self.node.product_id, 16))
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
|
||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||
" workaround")
|
||||
self._zxt_120 = 1
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Operation Mode
|
||||
self._current_operation = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, member='data')
|
||||
operation_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
member='data_items')
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
if self.values.mode:
|
||||
self._current_operation = self.values.mode.data
|
||||
operation_list = self.values.mode.data_items
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s", self._current_operation)
|
||||
|
||||
# Current Temp
|
||||
self._current_temperature = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='data')
|
||||
device_unit = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='units')
|
||||
if device_unit is not None:
|
||||
self._unit = device_unit
|
||||
if self.values.temperature:
|
||||
self._current_temperature = self.values.temperature.data
|
||||
device_unit = self.values.temperature.units
|
||||
if device_unit is not None:
|
||||
self._unit = device_unit
|
||||
|
||||
# Fan Mode
|
||||
self._current_fan_mode = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data')
|
||||
fan_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data_items')
|
||||
if fan_list:
|
||||
self._fan_list = list(fan_list)
|
||||
if self.values.fan_mode:
|
||||
self._current_fan_mode = self.values.fan_mode.data
|
||||
fan_list = self.values.fan_mode.data_items
|
||||
if fan_list:
|
||||
self._fan_list = list(fan_list)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
_LOGGER.debug("self._current_fan_mode=%s",
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
self._current_swing_mode = (
|
||||
self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data'))
|
||||
swing_list = self.get_value(class_id=zwave.const
|
||||
.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data_items')
|
||||
if swing_list:
|
||||
self._swing_list = list(swing_list)
|
||||
if self.values.zxt_120_swing_mode:
|
||||
self._current_swing_mode = self.values.zxt_120_swing_mode.data
|
||||
swing_list = self.values.zxt_120_swing_mode.data_items
|
||||
if swing_list:
|
||||
self._swing_list = list(swing_list)
|
||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
temps = []
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
temps.append((round(float(value.data)), 1))
|
||||
if value.index == self._index:
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
break
|
||||
else:
|
||||
self._target_temperature = round((float(value.data)), 1)
|
||||
if self.values.primary.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
else:
|
||||
self._target_temperature = round(
|
||||
(float(self.values.primary.data)), 1)
|
||||
|
||||
# Operating state
|
||||
self._operating_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
|
||||
member='data')
|
||||
if self.values.operating_state:
|
||||
self._operating_state = self.values.operating_state.data
|
||||
|
||||
# Fan operating state
|
||||
self._fan_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE,
|
||||
member='data')
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling on Z-Wave."""
|
||||
return False
|
||||
if self.values.fan_state:
|
||||
self._fan_state = self.values.fan_state.data
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
@@ -221,36 +183,31 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
else:
|
||||
return
|
||||
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT,
|
||||
index=self._index, data=temperature)
|
||||
self.update_ha_state()
|
||||
self.values.primary.data = temperature
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
index=0, data=bytes(fan, 'utf-8'))
|
||||
if self.values.fan_mode:
|
||||
self.values.fan_mode.data = bytes(fan, 'utf-8')
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
index=0, data=bytes(operation_mode, 'utf-8'))
|
||||
if self.values.mode:
|
||||
self.values.mode.data = bytes(operation_mode, 'utf-8')
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
if self._zxt_120 == 1:
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33, data=bytes(swing_mode, 'utf-8'))
|
||||
if self.values.zxt_120_swing_mode:
|
||||
self.values.zxt_120_swing_mode.data = bytes(
|
||||
swing_mode, 'utf-8')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
data = super().device_state_attributes
|
||||
if self._operating_state:
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state,
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state
|
||||
if self._fan_state:
|
||||
data[ATTR_FAN_STATE] = self._fan_state
|
||||
return data
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Component to configure Home Assistant via an API."""
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.setup import (
|
||||
async_prepare_setup_platform, ATTR_COMPONENT)
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.util.yaml import load_yaml, dump
|
||||
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = ('core', 'group', 'hassbian')
|
||||
ON_DEMAND = ('zwave', )
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the config component."""
|
||||
register_built_in_panel(hass, 'config', 'Configuration', 'mdi:settings')
|
||||
|
||||
@asyncio.coroutine
|
||||
def setup_panel(panel_name):
|
||||
"""Setup a panel."""
|
||||
panel = yield from async_prepare_setup_platform(hass, config, DOMAIN,
|
||||
panel_name)
|
||||
|
||||
if not panel:
|
||||
return
|
||||
|
||||
success = yield from panel.async_setup(hass)
|
||||
|
||||
if success:
|
||||
key = '{}.{}'.format(DOMAIN, panel_name)
|
||||
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key})
|
||||
hass.config.components.add(key)
|
||||
|
||||
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
|
||||
|
||||
for panel_name in ON_DEMAND:
|
||||
if panel_name in hass.config.components:
|
||||
tasks.append(setup_panel(panel_name))
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@callback
|
||||
def component_loaded(event):
|
||||
"""Respond to components being loaded."""
|
||||
panel_name = event.data.get(ATTR_COMPONENT)
|
||||
if panel_name in ON_DEMAND:
|
||||
hass.async_add_job(setup_panel(panel_name))
|
||||
|
||||
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EditKeyBasedConfigView(HomeAssistantView):
|
||||
"""Configure a Group endpoint."""
|
||||
|
||||
def __init__(self, component, config_type, path, key_schema, data_schema,
|
||||
*, post_write_hook=None):
|
||||
"""Initialize a config view."""
|
||||
self.url = '/api/config/%s/%s/{config_key}' % (component, config_type)
|
||||
self.name = 'api:config:%s:%s' % (component, config_type)
|
||||
self.path = path
|
||||
self.key_schema = key_schema
|
||||
self.data_schema = data_schema
|
||||
self.post_write_hook = post_write_hook
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, config_key):
|
||||
"""Fetch device specific config."""
|
||||
hass = request.app['hass']
|
||||
current = yield from hass.loop.run_in_executor(
|
||||
None, _read, hass.config.path(self.path))
|
||||
return self.json(current.get(config_key, {}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, config_key):
|
||||
"""Validate config and return results."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified', 400)
|
||||
|
||||
try:
|
||||
self.key_schema(config_key)
|
||||
except vol.Invalid as err:
|
||||
return self.json_message('Key malformed: {}'.format(err), 400)
|
||||
|
||||
try:
|
||||
# We just validate, we don't store that data because
|
||||
# we don't want to store the defaults.
|
||||
self.data_schema(data)
|
||||
except vol.Invalid as err:
|
||||
return self.json_message('Message malformed: {}'.format(err), 400)
|
||||
|
||||
hass = request.app['hass']
|
||||
path = hass.config.path(self.path)
|
||||
|
||||
current = yield from hass.loop.run_in_executor(None, _read, path)
|
||||
current.setdefault(config_key, {}).update(data)
|
||||
|
||||
yield from hass.loop.run_in_executor(None, _write, path, current)
|
||||
|
||||
if self.post_write_hook is not None:
|
||||
hass.async_add_job(self.post_write_hook(hass))
|
||||
|
||||
return self.json({
|
||||
'result': 'ok',
|
||||
})
|
||||
|
||||
|
||||
def _read(path):
|
||||
"""Read YAML helper."""
|
||||
if not os.path.isfile(path):
|
||||
with open(path, 'w'):
|
||||
pass
|
||||
return {}
|
||||
|
||||
return load_yaml(path)
|
||||
|
||||
|
||||
def _write(path, data):
|
||||
"""Write YAML helper."""
|
||||
# Do it before opening file. If dump causes error it will now not
|
||||
# truncate the file.
|
||||
data = dump(data)
|
||||
with open(path, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(data)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Component to interact with Hassbian tools."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.config import async_check_ha_config_file
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Setup the hassbian config."""
|
||||
hass.http.register_view(CheckConfigView)
|
||||
return True
|
||||
|
||||
|
||||
class CheckConfigView(HomeAssistantView):
|
||||
"""Hassbian packages endpoint."""
|
||||
|
||||
url = '/api/config/core/check_config'
|
||||
name = 'api:config:core:check_config'
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Validate config and return results."""
|
||||
errors = yield from async_check_ha_config_file(request.app['hass'])
|
||||
|
||||
state = 'invalid' if errors else 'valid'
|
||||
|
||||
return self.json({
|
||||
"result": state,
|
||||
"errors": errors,
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Provide configuration end points for Groups."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.config import EditKeyBasedConfigView
|
||||
from homeassistant.components.group import GROUP_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
CONFIG_PATH = 'groups.yaml'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Setup the Group config API."""
|
||||
hass.http.register_view(EditKeyBasedConfigView(
|
||||
'group', 'config', CONFIG_PATH, cv.slug,
|
||||
GROUP_SCHEMA
|
||||
))
|
||||
return True
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Component to interact with Hassbian tools."""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
|
||||
_TEST_OUTPUT = """
|
||||
{
|
||||
"suites":{
|
||||
"libcec":{
|
||||
"state":"Uninstalled",
|
||||
"description":"Installs the libcec package for controlling CEC devices from this Pi"
|
||||
},
|
||||
"mosquitto":{
|
||||
"state":"failed",
|
||||
"description":"Installs the Mosquitto package for setting up a local MQTT server"
|
||||
},
|
||||
"openzwave":{
|
||||
"state":"Uninstalled",
|
||||
"description":"Installs the Open Z-wave package for setting up your zwave network"
|
||||
},
|
||||
"samba":{
|
||||
"state":"installing",
|
||||
"description":"Installs the samba package for sharing the hassbian configuration files over the Pi's network."
|
||||
}
|
||||
}
|
||||
}
|
||||
""" # noqa
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Setup the hassbian config."""
|
||||
# Test if is hassbian
|
||||
test_mode = 'FORCE_HASSBIAN' in os.environ
|
||||
is_hassbian = test_mode
|
||||
|
||||
if not is_hassbian:
|
||||
return False
|
||||
|
||||
hass.http.register_view(HassbianSuitesView(test_mode))
|
||||
hass.http.register_view(HassbianSuiteInstallView(test_mode))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def hassbian_status(hass, test_mode=False):
|
||||
"""Query for the Hassbian status."""
|
||||
# fetch real output when not in test mode
|
||||
if test_mode:
|
||||
return json.loads(_TEST_OUTPUT)
|
||||
|
||||
raise Exception('Real mode not implemented yet.')
|
||||
|
||||
|
||||
class HassbianSuitesView(HomeAssistantView):
|
||||
"""Hassbian packages endpoint."""
|
||||
|
||||
url = '/api/config/hassbian/suites'
|
||||
name = 'api:config:hassbian:suites'
|
||||
|
||||
def __init__(self, test_mode):
|
||||
"""Initialize suites view."""
|
||||
self._test_mode = test_mode
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Request suite status."""
|
||||
inp = yield from hassbian_status(request.app['hass'], self._test_mode)
|
||||
|
||||
return self.json(inp['suites'])
|
||||
|
||||
|
||||
class HassbianSuiteInstallView(HomeAssistantView):
|
||||
"""Hassbian packages endpoint."""
|
||||
|
||||
url = '/api/config/hassbian/suites/{suite}/install'
|
||||
name = 'api:config:hassbian:suite'
|
||||
|
||||
def __init__(self, test_mode):
|
||||
"""Initialize suite view."""
|
||||
self._test_mode = test_mode
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, suite):
|
||||
"""Request suite status."""
|
||||
# do real install if not in test mode
|
||||
return self.json({"status": "ok"})
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Provide configuration end points for Z-Wave."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.config import EditKeyBasedConfigView
|
||||
from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
CONFIG_PATH = 'zwave_device_config.yaml'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Setup the Z-Wave config API."""
|
||||
hass.http.register_view(EditKeyBasedConfigView(
|
||||
'zwave', 'device_config', CONFIG_PATH, cv.entity_id,
|
||||
DEVICE_CONFIG_SCHEMA_ENTRY
|
||||
))
|
||||
return True
|
||||
@@ -6,15 +6,16 @@ This will return a request id that has to be used for future calls.
|
||||
A callback has to be provided to `request_config` which will be called when
|
||||
the user has submitted configuration information.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
_INSTANCES = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_REQUESTS = {}
|
||||
_KEY_INSTANCE = 'configurator'
|
||||
|
||||
ATTR_CONFIGURE_ID = 'configure_id'
|
||||
ATTR_DESCRIPTION = 'description'
|
||||
@@ -72,22 +73,20 @@ def request_done(request_id):
|
||||
pass
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the configurator component."""
|
||||
return True
|
||||
|
||||
|
||||
def _get_instance(hass):
|
||||
"""Get an instance per hass object."""
|
||||
try:
|
||||
return _INSTANCES[hass]
|
||||
except KeyError:
|
||||
_INSTANCES[hass] = Configurator(hass)
|
||||
instance = hass.data.get(_KEY_INSTANCE)
|
||||
|
||||
if DOMAIN not in hass.config.components:
|
||||
hass.config.components.append(DOMAIN)
|
||||
if instance is None:
|
||||
instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
|
||||
|
||||
return _INSTANCES[hass]
|
||||
return instance
|
||||
|
||||
|
||||
class Configurator(object):
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.14.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.15.0']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
|
||||
@@ -33,6 +33,20 @@ ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DEVICE_CLASSES = [
|
||||
'window', # Window control
|
||||
'garage', # Garage door control
|
||||
]
|
||||
|
||||
SUPPORT_OPEN = 1
|
||||
SUPPORT_CLOSE = 2
|
||||
SUPPORT_SET_POSITION = 4
|
||||
SUPPORT_STOP = 8
|
||||
SUPPORT_OPEN_TILT = 16
|
||||
SUPPORT_CLOSE_TILT = 32
|
||||
SUPPORT_STOP_TILT = 64
|
||||
SUPPORT_SET_TILT_POSITION = 128
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CURRENT_POSITION = 'current_position'
|
||||
@@ -152,7 +166,7 @@ def async_setup(hass, config):
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
cover.async_update_ha_state(True))
|
||||
if hasattr(cover, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
@@ -221,6 +235,21 @@ class CoverDevice(Entity):
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
|
||||
if self.current_cover_position is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
if self.current_cover_tilt_position is not None:
|
||||
supported_features |= (
|
||||
SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed or not."""
|
||||
|
||||
@@ -4,7 +4,8 @@ Demo platform for the cover component.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
|
||||
@@ -14,6 +15,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
DemoCover(hass, 'Kitchen Window'),
|
||||
DemoCover(hass, 'Hall Window', 10),
|
||||
DemoCover(hass, 'Living Room Window', 70, 50),
|
||||
DemoCover(hass, 'Garage Door', device_class='garage',
|
||||
supported_features=(SUPPORT_OPEN | SUPPORT_CLOSE)),
|
||||
])
|
||||
|
||||
|
||||
@@ -21,11 +24,14 @@ class DemoCover(CoverDevice):
|
||||
"""Representation of a demo cover."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, hass, name, position=None, tilt_position=None):
|
||||
def __init__(self, hass, name, position=None, tilt_position=None,
|
||||
device_class=None, supported_features=None):
|
||||
"""Initialize the cover."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._position = position
|
||||
self._device_class = device_class
|
||||
self._supported_features = supported_features
|
||||
self._set_position = None
|
||||
self._set_tilt_position = None
|
||||
self._tilt_position = tilt_position
|
||||
@@ -33,6 +39,10 @@ class DemoCover(CoverDevice):
|
||||
self._closing_tilt = True
|
||||
self._unsub_listener_cover = None
|
||||
self._unsub_listener_cover_tilt = None
|
||||
if position is None:
|
||||
self._closed = True
|
||||
else:
|
||||
self._closed = self.current_cover_position <= 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -57,17 +67,28 @@ class DemoCover(CoverDevice):
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self._closed
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
if self._supported_features is not None:
|
||||
return self._supported_features
|
||||
else:
|
||||
return None
|
||||
return super().supported_features
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
if self._position in (0, None):
|
||||
if self._position == 0:
|
||||
return
|
||||
elif self._position is None:
|
||||
self._closed = True
|
||||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
self._listen_cover()
|
||||
@@ -83,7 +104,11 @@ class DemoCover(CoverDevice):
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
if self._position in (100, None):
|
||||
if self._position == 100:
|
||||
return
|
||||
elif self._position is None:
|
||||
self._closed = False
|
||||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
self._listen_cover()
|
||||
@@ -149,6 +174,9 @@ class DemoCover(CoverDevice):
|
||||
|
||||
if self._position in (100, 0, self._set_position):
|
||||
self.stop_cover()
|
||||
|
||||
self._closed = self.current_cover_position <= 0
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _listen_cover_tilt(self):
|
||||
|
||||
@@ -168,6 +168,11 @@ class GaradgetCover(CoverDevice):
|
||||
else:
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'garage'
|
||||
|
||||
def get_token(self):
|
||||
"""Get new token for usage during this session."""
|
||||
args = {
|
||||
|
||||
@@ -10,28 +10,26 @@ properly configured.
|
||||
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.cover import CoverDevice,\
|
||||
ATTR_POSITION
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.cover import CoverDevice, ATTR_POSITION
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMCover,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, config)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class HMCover(HMDevice, CoverDevice):
|
||||
@@ -44,18 +42,15 @@ class HMCover(HMDevice, CoverDevice):
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
if self.available:
|
||||
return int(self._hm_get_state() * 100)
|
||||
return None
|
||||
return int(self._hm_get_state() * 100)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if self.available:
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = float(kwargs[ATTR_POSITION])
|
||||
position = min(100, max(0, position))
|
||||
level = position / 100.0
|
||||
self._hmdevice.set_level(level, self._channel)
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = float(kwargs[ATTR_POSITION])
|
||||
position = min(100, max(0, position))
|
||||
level = position / 100.0
|
||||
self._hmdevice.set_level(level, self._channel)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
@@ -68,18 +63,15 @@ class HMCover(HMDevice, CoverDevice):
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
if self.available:
|
||||
self._hmdevice.move_up(self._channel)
|
||||
self._hmdevice.move_up(self._channel)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
if self.available:
|
||||
self._hmdevice.move_down(self._channel)
|
||||
self._hmdevice.move_down(self._channel)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the device if in motion."""
|
||||
if self.available:
|
||||
self._hmdevice.stop(self._channel)
|
||||
self._hmdevice.stop(self._channel)
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from hm metadata."""
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for MQTT cover devices.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -46,13 +47,14 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the MQTT Cover."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
add_devices([MqttCover(
|
||||
hass,
|
||||
|
||||
async_add_devices([MqttCover(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
@@ -71,13 +73,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class MqttCover(CoverDevice):
|
||||
"""Representation of a cover that can be controlled using MQTT."""
|
||||
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
retain, state_open, state_closed, payload_open, payload_close,
|
||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
||||
state_open, state_closed, payload_open, payload_close,
|
||||
payload_stop, optimistic, value_template):
|
||||
"""Initialize the cover."""
|
||||
self._position = None
|
||||
self._state = None
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
@@ -89,37 +90,45 @@ class MqttCover(CoverDevice):
|
||||
self._state_closed = state_closed
|
||||
self._retain = retain
|
||||
self._optimistic = optimistic or state_topic is None
|
||||
self._template = value_template
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
|
||||
if payload == self._state_open:
|
||||
self._state = False
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._state_closed:
|
||||
self._state = True
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload.isnumeric() and 0 <= int(payload) <= 100:
|
||||
if int(payload) > 0:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
self._position = int(payload)
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Payload is not True, False, or integer (0-100): %s",
|
||||
payload)
|
||||
return
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
if self._state_topic is None:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
else:
|
||||
mqtt.subscribe(hass, self._state_topic, message_received,
|
||||
self._qos)
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -144,25 +153,40 @@ class MqttCover(CoverDevice):
|
||||
"""
|
||||
return self._position
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_open,
|
||||
self._qos, self._retain)
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_open, self._qos,
|
||||
self._retain)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = False
|
||||
self.schedule_update_ha_state()
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_close,
|
||||
self._qos, self._retain)
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_close, self._qos,
|
||||
self._retain)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = True
|
||||
self.schedule_update_ha_state()
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the device."""
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_stop,
|
||||
self._qos, self._retain)
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Stop the device.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_stop, self._qos,
|
||||
self._retain)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Support for MyQ-Enabled Garage Doors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/cover.myq/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
|
||||
'#pymyq==0.0.8']
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
DEFAULT_NAME = 'myq'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the MyQ component."""
|
||||
from pymyq import MyQAPI as pymyq
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
brand = config.get(CONF_TYPE)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
myq = pymyq(username, password, brand)
|
||||
|
||||
if not myq.is_supported_brand():
|
||||
logger.error('MyQ Cover - Unsupported Type. See documentation')
|
||||
return
|
||||
|
||||
if not myq.is_login_valid():
|
||||
logger.error('MyQ Cover - Username or Password is incorrect')
|
||||
return
|
||||
|
||||
try:
|
||||
add_devices(MyQDevice(myq, door) for door in myq.get_garage_doors())
|
||||
except (TypeError, KeyError, NameError) as ex:
|
||||
logger.error("MyQ Cover - %s", ex)
|
||||
|
||||
|
||||
class MyQDevice(CoverDevice):
|
||||
"""Representation of a MyQ cover."""
|
||||
|
||||
def __init__(self, myq, device):
|
||||
"""Initialize with API object, device id."""
|
||||
self.myq = myq
|
||||
self.device_id = device['deviceid']
|
||||
self._name = device['name']
|
||||
self._status = STATE_CLOSED
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll for state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door if any."""
|
||||
return self._name if self._name else DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return True if cover is closed, else False."""
|
||||
return self._status == STATE_CLOSED
|
||||
|
||||
def close_cover(self):
|
||||
"""Issue close command to cover."""
|
||||
self.myq.close_device(self.device_id)
|
||||
|
||||
def open_cover(self):
|
||||
"""Issue open command to cover."""
|
||||
self.myq.open_device(self.device_id)
|
||||
|
||||
def update(self):
|
||||
"""Update status of cover."""
|
||||
self._status = self.myq.get_status(self.device_id)
|
||||
@@ -6,9 +6,9 @@ https://home-assistant.io/components/cover.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@@ -28,6 +28,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
||||
@@ -7,80 +7,62 @@ https://home-assistant.io/components/cover.zwave/
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.cover import DOMAIN
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.components.zwave import workaround
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
|
||||
SOMFY = 0x47
|
||||
SOMFY_ZRTSI = 0x5a52
|
||||
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
|
||||
WORKAROUND = 'workaround'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
SOMFY_ZRTSI_CONTROLLER: WORKAROUND
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave covers."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
|
||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and value.index == 0):
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if (value.type != zwave.const.TYPE_BOOL and
|
||||
value.genre != zwave.const.GENRE_USER):
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
else:
|
||||
return
|
||||
def get_device(values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
if (values.primary.command_class ==
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and values.primary.index == 0):
|
||||
return ZwaveRollershutter(values)
|
||||
elif (values.primary.command_class in [
|
||||
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
||||
return ZwaveGarageDoor(values)
|
||||
return None
|
||||
|
||||
|
||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave roller shutter."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, values):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._node = value.node
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
self._current_position = None
|
||||
self._workaround = None
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_type, 16))
|
||||
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND:
|
||||
_LOGGER.debug("Controller without positioning feedback")
|
||||
self._workaround = 1
|
||||
self._workaround = workaround.get_device_mapping(values.primary)
|
||||
if self._workaround:
|
||||
_LOGGER.debug("Using workaround %s", self._workaround)
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Position value
|
||||
self._current_position = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Level'], member='data')
|
||||
self._open_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Open', 'Up'], member='value_id')
|
||||
self._close_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Close', 'Down'], member='value_id')
|
||||
self._current_position = self.values.primary.data
|
||||
|
||||
if self.values.open and self.values.close and \
|
||||
self._open_id is None and self._close_id is None:
|
||||
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
|
||||
self._open_id = self.values.close.value_id
|
||||
self._close_id = self.values.open.value_id
|
||||
self._workaround = None
|
||||
else:
|
||||
self._open_id = self.values.open.value_id
|
||||
self._close_id = self.values.close.value_id
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
@@ -95,14 +77,15 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of Zwave roller shutter."""
|
||||
if not self._workaround:
|
||||
if self._current_position is not None:
|
||||
if self._current_position <= 5:
|
||||
return 0
|
||||
elif self._current_position >= 95:
|
||||
return 100
|
||||
else:
|
||||
return self._current_position
|
||||
if self._workaround == workaround.WORKAROUND_NO_POSITION:
|
||||
return None
|
||||
if self._current_position is not None:
|
||||
if self._current_position <= 5:
|
||||
return 0
|
||||
elif self._current_position >= 95:
|
||||
return 100
|
||||
else:
|
||||
return self._current_position
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
@@ -114,7 +97,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
self._node.set_dimmer(self._value.value_id, position)
|
||||
self.node.set_dimmer(self.values.primary.value_id, position)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
@@ -124,19 +107,34 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave garage door device."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, values):
|
||||
"""Initialize the zwave garage door."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self.values.primary.data
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of Zwave garage door."""
|
||||
return not self._value.data
|
||||
return not self._state
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the garage door."""
|
||||
self._value.data = False
|
||||
self.values.primary.data = False
|
||||
|
||||
def open_cover(self):
|
||||
"""Open the garage door."""
|
||||
self._value.data = True
|
||||
self.values.primary.data = True
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'garage'
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_GARAGE
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user