Compare commits
607 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50bf147d73 | |||
| 3bdf7eabbf | |||
| 4e750a4d72 | |||
| a1cd120ed4 | |||
| be55ee059e | |||
| eb8228237e | |||
| afb51d14b8 | |||
| bca4dee30e | |||
| 9e4ddc405d | |||
| 73ec049d1c | |||
| efeb5c5290 | |||
| 498a3f78d4 | |||
| 562db5ea4c | |||
| cd21142d5b | |||
| 9b371a8c66 | |||
| d7e3c6a442 | |||
| f4b5b3f88f | |||
| a0c58487ca | |||
| f8f0ed860c | |||
| f99a536b98 | |||
| 02faefdab3 | |||
| 2a01b09d31 | |||
| 712ba65d26 | |||
| 124a9b7a81 | |||
| cbd69583bd | |||
| 2ec141b5d4 | |||
| 2609852d6e | |||
| e525b6e0a5 | |||
| 278fdc0983 | |||
| 6dc49df2df | |||
| b2d32114c8 | |||
| 6512b7d701 | |||
| 782713ee4f | |||
| dd45b5e0b1 | |||
| f92802fd5c | |||
| ddbceebd65 | |||
| 37e5b98919 | |||
| 86973226b1 | |||
| bbf8897a51 | |||
| 72144945b4 | |||
| 5a64ef2c98 | |||
| 393df2da49 | |||
| a54986159c | |||
| 8ffa3684e3 | |||
| dc02370b43 | |||
| 23db6e753f | |||
| 35aa9aa863 | |||
| 57ac6cd76f | |||
| a7519bb38b | |||
| db4bf7319f | |||
| 621a6e0ea0 | |||
| caa1b73035 | |||
| 9c538d11c2 | |||
| 2790ee0b02 | |||
| eb681e721c | |||
| db06d5a03d | |||
| eb5f208a09 | |||
| b096004449 | |||
| c2729211ad | |||
| 8840461af3 | |||
| 5406028ace | |||
| 4563c54a3e | |||
| 04b48e0683 | |||
| 3f82b9d6b0 | |||
| 2b839de854 | |||
| 967a751da1 | |||
| 8dc6443d14 | |||
| 537808f9be | |||
| 49e588deb3 | |||
| 6b4dd64cfe | |||
| a47d6c944e | |||
| 582394bc3b | |||
| 213cc920d0 | |||
| 05a1e11db2 | |||
| f8240a9cda | |||
| d6a14a1767 | |||
| 9b0b3c474e | |||
| 532d807771 | |||
| 60d579af84 | |||
| c64da761f1 | |||
| 2d3d674295 | |||
| 590512916a | |||
| d398832112 | |||
| 88dc7a08c4 | |||
| 7cc3b8d7b1 | |||
| 09ff394abc | |||
| b294383f20 | |||
| 5bd0b5ab99 | |||
| 7d9e882d52 | |||
| 1f842140ef | |||
| 8d5a164346 | |||
| cbe27d8f1a | |||
| 066b569cfe | |||
| fd3ea95b82 | |||
| e4485dcf3d | |||
| 7a0c99a1d5 | |||
| 7c15f6a4b3 | |||
| afe564fb3f | |||
| 82e11e237e | |||
| 4dad40fffb | |||
| 368ad93eb6 | |||
| 92be572374 | |||
| c77266c544 | |||
| aa748e3e48 | |||
| 3d8e9b4261 | |||
| 7c6dcdb082 | |||
| c3bb6d32aa | |||
| 9f5f13644a | |||
| 7805d2800c | |||
| 7dd529356a | |||
| 070cee48a8 | |||
| b961b5037f | |||
| 3da554a198 | |||
| d1a4dc77d1 | |||
| ff9568ad26 | |||
| 2f66528595 | |||
| 9ad2cf7b7a | |||
| 6ac54b20c7 | |||
| 6847dac582 | |||
| 7f81122af6 | |||
| 1eae74be58 | |||
| 443b39bccd | |||
| 8f70630790 | |||
| 5053c807c0 | |||
| 6e7ca9505c | |||
| f3c95adaca | |||
| 6532eae3d5 | |||
| ff6e071dff | |||
| cd6d44ece3 | |||
| 22b47ce9c6 | |||
| f4e0f1d895 | |||
| 8a1fa82205 | |||
| fb9d8c79b5 | |||
| f5f52010d1 | |||
| 1bfea626ff | |||
| 4c538c718b | |||
| 63a27f1943 | |||
| 70aa605396 | |||
| 08aaea5444 | |||
| 2d0721abe8 | |||
| 4128d083e6 | |||
| 52131a3335 | |||
| 27f456ca70 | |||
| f9385eb87a | |||
| 1c8c16f85f | |||
| c85875ddf4 | |||
| fa6d9adcba | |||
| 4e6b755b26 | |||
| d019b7f6b8 | |||
| a75833cf2b | |||
| f20ea41538 | |||
| 97e1f0be98 | |||
| 85bf9a49ea | |||
| 9e48b88154 | |||
| 14afd1e409 | |||
| ee62120fe5 | |||
| 7bd4e58b9d | |||
| 0417803b7c | |||
| d65c1c2b0d | |||
| 1841f3994f | |||
| 5f3cb58d8e | |||
| e8da63db28 | |||
| e80309c03c | |||
| 233a2a2878 | |||
| e677fc876b | |||
| 560173e13b | |||
| d57df2ddde | |||
| 9a740bff2a | |||
| acea32949b | |||
| 30e1569b21 | |||
| d7c4b50e3e | |||
| d93883f153 | |||
| 2d932f89fc | |||
| 182fcb5f03 | |||
| 98d18c3060 | |||
| 6578928e3c | |||
| c1d39a2fce | |||
| 9f7ce23e80 | |||
| d39e1be388 | |||
| 61b0d02a88 | |||
| 8eb396a435 | |||
| 70f05f30d5 | |||
| 9ac53b502f | |||
| 6e8c79d531 | |||
| 03423cc3a9 | |||
| cd6780baf4 | |||
| bb37708716 | |||
| 0bacc7be07 | |||
| 99ac4524b9 | |||
| 348b7abe7d | |||
| cf93644d54 | |||
| 4d8487e7ba | |||
| 96dde18ae3 | |||
| 9aa4028718 | |||
| 966b83f648 | |||
| b2366ce68e | |||
| bd3f0c101d | |||
| ab9ac80ee0 | |||
| aea0598805 | |||
| b7c4370e2b | |||
| 22865e5d96 | |||
| c66511e0cf | |||
| 79dbd14568 | |||
| 5756cff8a4 | |||
| 44a39636b1 | |||
| 307b2c629b | |||
| bbfa63ee79 | |||
| 008d65677b | |||
| 68973dbc4d | |||
| 9a6c99264e | |||
| 1d7aea7120 | |||
| 16865b82d3 | |||
| 2ba3df2257 | |||
| aec269050f | |||
| b93ebe1936 | |||
| 0010cadd39 | |||
| 75f2b4bc0e | |||
| a7ce9ba49e | |||
| f429a6c4ff | |||
| 3d83eea5f7 | |||
| a3e8994fcc | |||
| eacfac6fa8 | |||
| 0d45470ea6 | |||
| bca3207e0c | |||
| de7a14074e | |||
| cdc93ab670 | |||
| c287520432 | |||
| 68803a46b6 | |||
| 8d366a7367 | |||
| 23b116803b | |||
| 3610f40a6a | |||
| 366595fd90 | |||
| 00486dccea | |||
| 09ab3e95c0 | |||
| fa8857dfc5 | |||
| bade0e0d71 | |||
| dd2aec0a08 | |||
| 64430f26f3 | |||
| 4eecdbdab1 | |||
| 92a11819b4 | |||
| dabb8d5bbc | |||
| 8c67a924bc | |||
| 97c0f5bb5a | |||
| aeb87b0245 | |||
| 6be2ec7fea | |||
| 885b61a750 | |||
| 0ba7fb40a4 | |||
| 263839a336 | |||
| 3ced457089 | |||
| 01df1f8458 | |||
| b29f2f6d6f | |||
| cafa4043b3 | |||
| 8bea5c06de | |||
| 06de73ff80 | |||
| ada2fb4ec0 | |||
| 39bbfd14d9 | |||
| 9e816cfd3f | |||
| e170484f16 | |||
| 9204c44eec | |||
| 0fbd947426 | |||
| 5921e65d83 | |||
| c51dd64bd8 | |||
| 278033cbf9 | |||
| 9233449551 | |||
| 680f450278 | |||
| 66c2fa1270 | |||
| bf0b453677 | |||
| 29b6782b42 | |||
| d2df485bea | |||
| 77b141a355 | |||
| 00afaac54c | |||
| 34b91cf6ce | |||
| 9dc055e537 | |||
| 6d6cf886f3 | |||
| 1571b33e4a | |||
| a29be5455c | |||
| 4210291e5b | |||
| e9fa1f1f83 | |||
| 724d5bfe9d | |||
| 938c9888a6 | |||
| b91e4cfb4a | |||
| 7dd51034cd | |||
| 2f60ff224f | |||
| fc3a37cba2 | |||
| 223b7f85ee | |||
| 13b0beee31 | |||
| ba530e5a16 | |||
| aafd36d2ce | |||
| 0492f0abd0 | |||
| 940799d0da | |||
| c584b6b28d | |||
| 5a03ddd7e0 | |||
| 1ba60dc898 | |||
| 75d3d25969 | |||
| 06bd812b7b | |||
| 7cb57583e2 | |||
| 3e3f5db2a5 | |||
| 88fe28ea1b | |||
| 10a20f802b | |||
| 9521dad263 | |||
| 8857c48c17 | |||
| 3c582d1e3c | |||
| 19d12716ef | |||
| 57446cfb08 | |||
| ff6cb2b452 | |||
| a3ec7998b1 | |||
| d892716bfa | |||
| 61e2da8827 | |||
| abc039a3d0 | |||
| 7ba7747ce8 | |||
| b04ff7207c | |||
| 72235c48fb | |||
| 3f7ff2b1d4 | |||
| 484b7b64d7 | |||
| 7241762bcc | |||
| 0a904acd4d | |||
| 76df759f4c | |||
| 4a2b956493 | |||
| 3aa34deaa2 | |||
| b00cad7095 | |||
| 2cf061c768 | |||
| e837e97c9d | |||
| df8afe51f4 | |||
| c8e6f89302 | |||
| 2e75a58372 | |||
| ae2fd149a5 | |||
| ee62c2cc2b | |||
| 4cfa14c29d | |||
| 4ce1a67c13 | |||
| 962463c1ab | |||
| 74f06b6862 | |||
| d3d7d458e1 | |||
| cca6b0c287 | |||
| 23e3b8d2f2 | |||
| be9a2a043e | |||
| cc4fa6cd38 | |||
| 4d15367956 | |||
| 175b49236c | |||
| fd0afaa204 | |||
| 034cec7152 | |||
| f464d591c9 | |||
| 06cb97adee | |||
| cab46b91e3 | |||
| 0da09b85de | |||
| 95d9bc48ea | |||
| 6b962a2207 | |||
| 18b3d3df57 | |||
| 5f6977acda | |||
| 89f6ef9f6c | |||
| d2ad0620ee | |||
| aa13392983 | |||
| 6cbf19934f | |||
| f938134069 | |||
| 7cdcb800a9 | |||
| 91fb2764cc | |||
| 82c5e2cf3c | |||
| bb8981b611 | |||
| b350f22a77 | |||
| 40da28a0c7 | |||
| 3bdb50510a | |||
| 7478c36b27 | |||
| b1f2c90bd0 | |||
| e53785f30c | |||
| 1a38354ed5 | |||
| 02609d0ab5 | |||
| ce4f5ff29c | |||
| 5190cc74c5 | |||
| e83f8da342 | |||
| ddaeeba68b | |||
| 75775a561b | |||
| 058315720f | |||
| 4e0c7f8a3d | |||
| c705ca4288 | |||
| 15ad48a7a0 | |||
| c9c15c4cf7 | |||
| 3046bfce7b | |||
| d52e2019c0 | |||
| af8f6bcaba | |||
| cdf0e80773 | |||
| b0948bef5f | |||
| 70a528c04b | |||
| d796625098 | |||
| dc44ef7356 | |||
| c5c4085ad4 | |||
| 09b3aba51b | |||
| 6f3aefde64 | |||
| b3a1491482 | |||
| b7ff79da24 | |||
| 4bf4d94344 | |||
| 3e26af5ff1 | |||
| c1270cf0bb | |||
| 26fc637ab5 | |||
| 66c5d96b43 | |||
| 41f908ed39 | |||
| 0f5487b95a | |||
| 23c5159f6c | |||
| 4840dd297a | |||
| 4605742bb7 | |||
| f222340c8e | |||
| b17df44402 | |||
| 895ddc8433 | |||
| d867d26612 | |||
| 564e328698 | |||
| 160b811ddf | |||
| 2e164e519a | |||
| 779188ad27 | |||
| 3f6349d663 | |||
| ac0dc10377 | |||
| 0a7db98b0e | |||
| 8f690ff077 | |||
| 951fa603ff | |||
| c113997609 | |||
| f700635445 | |||
| d49fae86e4 | |||
| 64611ab2be | |||
| 27dc2f61fb | |||
| cd25c8f72d | |||
| 9ad1d290af | |||
| 90ef81d8d5 | |||
| 02efe903ab | |||
| 0bb63bf3f0 | |||
| e23db5d972 | |||
| e311f89056 | |||
| 757946293e | |||
| 98c6e56ea4 | |||
| cd0cef6403 | |||
| 0d2891ebcc | |||
| fb6aded2e1 | |||
| 8b7cfc831d | |||
| 987be65d55 | |||
| f08b77dc4c | |||
| 681b84e1bd | |||
| 4103d7463b | |||
| 428750eeda | |||
| 0ae36e1d28 | |||
| d2e8721918 | |||
| bbdc196127 | |||
| a417156d84 | |||
| 3575ddb6ef | |||
| ffc4822f50 | |||
| a147304be9 | |||
| a001780afb | |||
| 7a00bf8696 | |||
| 7eef831ff3 | |||
| 9fde97efed | |||
| d773ad1ecb | |||
| cab1100a51 | |||
| 3616d7a7ea | |||
| d38ad57b7d | |||
| b3e966665a | |||
| 6e69737e88 | |||
| 062fe79b3f | |||
| af0a44d976 | |||
| 43613f000d | |||
| dde80850a6 | |||
| 11a2b8888b | |||
| b700ec4faa | |||
| 614034d196 | |||
| c9d145cb13 | |||
| b6a32098d1 | |||
| 4cf85294db | |||
| 6fc68e9c8a | |||
| ec88733b57 | |||
| 7ef2075520 | |||
| fbd0dbf8ee | |||
| 2ba237eac8 | |||
| 6bf4532608 | |||
| 2622cf2e53 | |||
| a5db23afa4 | |||
| 2c4166b5f2 | |||
| 8be9aaba4f | |||
| 1b16d76c40 | |||
| 1a6539ad41 | |||
| 96066e94ab | |||
| 78e758925b | |||
| 19fc48f4a0 | |||
| d469970e5a | |||
| 50a9b3a7c0 | |||
| ab837f9070 | |||
| 6149e509c3 | |||
| f3b74079e0 | |||
| c580953bd8 | |||
| fc3741911c | |||
| 2589e78e84 | |||
| ced380f0cd | |||
| a33f1c61e5 | |||
| 8cf5ca0ba8 | |||
| b20d3f8b3a | |||
| 1f34b3586e | |||
| 37dadd1ae0 | |||
| fac8d4b969 | |||
| efcba8f1ca | |||
| abc253c4c5 | |||
| 3d00735341 | |||
| ce75c590b1 | |||
| 6e6c3c5cd5 | |||
| 5521096c02 | |||
| 356013118d | |||
| 9a9dbcfaea | |||
| 1c33e01b99 | |||
| d09837fef6 | |||
| f5e736d271 | |||
| 61630783f1 | |||
| 077797ac4f | |||
| b14f7f7ed0 | |||
| 3d695405b7 | |||
| 55932b048e | |||
| b19fbd8e72 | |||
| 635369ad65 | |||
| bd8881cbe1 | |||
| 847e92f57a | |||
| 5cea8fda9f | |||
| 7f87df20c2 | |||
| e91c8e4143 | |||
| cd00ff8b56 | |||
| 018329b12b | |||
| a955f3db08 | |||
| d344defc7e | |||
| 2da422fd77 | |||
| 93a38d39ef | |||
| 3aad223c95 | |||
| 1a5d18fd66 | |||
| e7e540d4bb | |||
| 35613d7fbf | |||
| 00d1cab091 | |||
| 26efaa91a3 | |||
| 3c37ecc477 | |||
| 274aaabd93 | |||
| c8bfd27182 | |||
| 08ab7dba2c | |||
| 54cc35d729 | |||
| 031e7a4013 | |||
| 41165695f0 | |||
| 9c33af60f2 | |||
| 7c1241c1f8 | |||
| 9caa4752a4 | |||
| cb2e75befd | |||
| d54e10e54a | |||
| 95748a6880 | |||
| 10a41a22dc | |||
| ac0b6ca50c | |||
| a0f6f3ac22 | |||
| d9aff0c76d | |||
| 5005b20122 | |||
| b3ef2bd2d9 | |||
| e29a2fa45a | |||
| 395743005a | |||
| 8de56bc8e2 | |||
| 79b6269aa2 | |||
| 525b206e1b | |||
| 2f4e40db27 | |||
| 38c9f7a37a | |||
| c725f7883a | |||
| 10f79ab45d | |||
| 455593017d | |||
| d6b19aae48 | |||
| 6519333e1d | |||
| 41919e7339 | |||
| 1c10f218de | |||
| 96710ad410 | |||
| c95c3d9198 | |||
| 5719743ec7 | |||
| e2e8d4276f | |||
| 3f03fefd35 | |||
| 2dab815f90 | |||
| 6a4b63f807 | |||
| 8eef978241 | |||
| 1789a08d21 | |||
| de4dab74b1 | |||
| 16b1529d14 | |||
| 0b8e097705 | |||
| b21be63220 | |||
| e6a8746dba | |||
| 1974eda51d | |||
| bd475f5db1 | |||
| fce8815ab4 | |||
| 90e17fc77f | |||
| 6418634f3a | |||
| a230d00ed0 | |||
| 5fdbe5fd9a | |||
| 283d621e90 | |||
| 2d0004f46a | |||
| 6a08f14120 | |||
| 97e867052d | |||
| 2651021461 | |||
| 4b253d17ba | |||
| b7722ec452 | |||
| fd6086a5d6 | |||
| 3e35bc06fc | |||
| f76dee8a05 | |||
| 56ac4281c7 | |||
| b8e149fe7d | |||
| 4a8f55e630 | |||
| de61bcb80e | |||
| 4cc9606bcc | |||
| 8ac763c6f6 | |||
| 6a75b524cb | |||
| c1d057407b | |||
| c396dbb570 | |||
| 0631f5c59d | |||
| 10f9c049bb | |||
| 106c53abf1 | |||
| b56369855a | |||
| a41b66bb94 | |||
| 3534c975f3 | |||
| 519abbbfa2 | |||
| b596fa33d6 |
+28
-7
@@ -6,10 +6,17 @@ omit =
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
homeassistant/components/apcupsd.py
|
||||
homeassistant/components/*/apcupsd.py
|
||||
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
@@ -32,6 +39,9 @@ omit =
|
||||
homeassistant/components/verisure.py
|
||||
homeassistant/components/*/verisure.py
|
||||
|
||||
homeassistant/components/wemo.py
|
||||
homeassistant/components/*/wemo.py
|
||||
|
||||
homeassistant/components/wink.py
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
@@ -53,10 +63,16 @@ omit =
|
||||
homeassistant/components/rpi_gpio.py
|
||||
homeassistant/components/*/rpi_gpio.py
|
||||
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/*
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/generic.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
@@ -73,9 +89,8 @@ omit =
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/statsd.py
|
||||
homeassistant/components/influxdb.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/hue.py
|
||||
@@ -89,24 +104,29 @@ omit =
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/samsungtv.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/googlevoice.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
homeassistant/components/notify/pushetta.py
|
||||
homeassistant/components/notify/pushover.py
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/sendgrid.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/notify/googlevoice.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dweet.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
@@ -114,10 +134,13 @@ omit =
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/netatmo.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
@@ -128,19 +151,17 @@ omit =
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wemo.py
|
||||
homeassistant/components/thermostat/heatmiser.py
|
||||
homeassistant/components/thermostat/homematic.py
|
||||
homeassistant/components/thermostat/honeywell.py
|
||||
homeassistant/components/thermostat/proliphix.py
|
||||
homeassistant/components/thermostat/radiotherm.py
|
||||
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
**Home Assistant release (`hass --version`):**
|
||||
|
||||
|
||||
**Python release (`python3 --version`):**
|
||||
|
||||
|
||||
**Component/platform:**
|
||||
|
||||
|
||||
**Description of problem:**
|
||||
|
||||
|
||||
**Expected:**
|
||||
|
||||
|
||||
**Problem-relevant `configuration.yaml` entries and steps to reproduce:**
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Traceback (if applicable):**
|
||||
```bash
|
||||
|
||||
```
|
||||
|
||||
**Additional info:**
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
**Description:**
|
||||
|
||||
|
||||
**Related issue (if applicable):** #
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
|
||||
- [ ] Local tests with `tox` ran successfully.
|
||||
- [ ] No CI failures. **Your PR cannot be merged unless CI is green!**
|
||||
- [ ] [Fork is up to date][fork] and was rebased on the `dev` branch before creating the PR.
|
||||
- If code communicates with devices:
|
||||
- [ ] 3rd party library/libraries for communication is/are added as dependencies via the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] 3rd party dependencies are imported inside functions that use them ([example][ex-import]).
|
||||
- [ ] `requirements_all.txt` is up-to-date, `script/gen_requirements_all.py` ran and the updated file is included in the PR.
|
||||
- [ ] New files were added to `.coveragerc`.
|
||||
- If the code does not depend on external Python module:
|
||||
- [ ] Tests to verify that the code works are included.
|
||||
- [ ] [Commits will be squashed][squash] when the PR is ready to be merged.
|
||||
|
||||
[fork]: http://stackoverflow.com/a/7244456
|
||||
[squash]: https://github.com/ginatrapani/todo.txt-android/wiki/Squash-All-Commits-Related-to-a-Single-Issue-into-a-Single-Commit
|
||||
[ex-requir]: https://github.com/balloob/home-assistant/blob/dev/homeassistant/components/keyboard.py#L16
|
||||
[ex-import]: https://github.com/balloob/home-assistant/blob/dev/homeassistant/components/keyboard.py#L51
|
||||
|
||||
+15
-11
@@ -1,15 +1,19 @@
|
||||
sudo: false
|
||||
language: python
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.4"
|
||||
env: TOXENV=py34
|
||||
- python: "3.4"
|
||||
env: TOXENV=requirements
|
||||
- python: "3.5"
|
||||
env: TOXENV=lint
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
# - "$HOME/virtualenv/python$TRAVIS_PYTHON_VERSION"
|
||||
python:
|
||||
- 3.4
|
||||
- 3.5
|
||||
install:
|
||||
- "true"
|
||||
script:
|
||||
- script/cibuild
|
||||
matrix:
|
||||
fast_finish: true
|
||||
install: pip install -U tox coveralls
|
||||
language: python
|
||||
script: tox
|
||||
after_success: coveralls
|
||||
|
||||
+20
-4
@@ -6,7 +6,7 @@ The process is straight-forward.
|
||||
|
||||
- Fork the Home Assistant [git repository](https://github.com/balloob/home-assistant).
|
||||
- Write the code for your device, notification service, sensor, or IoT thing.
|
||||
- Check it with ``pylint`` and ``flake8``.
|
||||
- Ensure tests work.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant.
|
||||
|
||||
Still interested? Then you should read the next sections and get more details.
|
||||
@@ -17,12 +17,13 @@ For help on building your component, please see the [developer documentation](ht
|
||||
|
||||
After you finish adding support for your device:
|
||||
|
||||
- Check that all dependencies are included via the `REQUIREMENTS` variable in your platform/component and only imported inside functions that use them.
|
||||
- Add any new dependencies to `requirements_all.txt` if needed. Use `script/gen_requirements_all.py`.
|
||||
- Update the `.coveragerc` file to exclude your platform if there are no tests available.
|
||||
- Update the `.coveragerc` file to exclude your platform if there are no tests available or your new code uses a 3rd party library for communication with the device/service/sensor.
|
||||
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). It's OK to just add a docstring with configuration details (sample entry for `configuration.yaml` file and alike) to the file header as a start. Visit the [website documentation](https://home-assistant.io/developers/website/) for further information on contributing to [home-assistant.io](https://github.com/balloob/home-assistant.io).
|
||||
- Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `./script/lint`.
|
||||
- Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `tox` or `script/lint`.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant.
|
||||
- Check for comments and suggestions on your Pull Request and keep an eye on the [Travis output](https://travis-ci.org/balloob/home-assistant/).
|
||||
- Check for comments and suggestions on your Pull Request and keep an eye on the [CI output](https://travis-ci.org/balloob/home-assistant/).
|
||||
|
||||
If you add a platform for an existing component, there is usually no need for updating the frontend. Only if you've added a new component that should show up in the frontend, there are more steps needed:
|
||||
|
||||
@@ -66,6 +67,21 @@ The frontend is composed of [Polymer](https://www.polymer-project.org) web-compo
|
||||
|
||||
When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works.
|
||||
|
||||
## Testing your code
|
||||
|
||||
To test your code before submission, used the `tox` tool.
|
||||
|
||||
```shell
|
||||
> pip install -U tox
|
||||
> tox
|
||||
```
|
||||
|
||||
This will run unit tests against python 3.4 and 3.5 (if both are available locally), as well as run a set of tests which validate `pep8` and `pylint` style of the code.
|
||||
|
||||
You can optionally run tests on only one tox target using the `-e` option to select an environment.
|
||||
|
||||
For instance `tox -e lint` will run the linters only, `tox -e py34` will run unit tests only on python 3.4.
|
||||
|
||||
### Notes on PyLint and PEP8 validation
|
||||
|
||||
In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change.
|
||||
|
||||
@@ -6,6 +6,8 @@ VOLUME /config
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN pip3 install --no-cache-dir colorlog
|
||||
|
||||
# For the nmap tracker
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nmap net-tools && \
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""
|
||||
custom_components.example
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Example of a custom component.
|
||||
|
||||
Example component to target an entity_id to:
|
||||
- turn it on at 7AM in the morning
|
||||
@@ -37,21 +36,21 @@ import homeassistant.components as core
|
||||
from homeassistant.components import device_tracker
|
||||
from homeassistant.components import light
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component
|
||||
# The domain of your component. Should be equal to the name of your component.
|
||||
DOMAIN = "example"
|
||||
|
||||
# List of component names (string) your component depends upon
|
||||
# List of component names (string) your component depends upon.
|
||||
# We depend on group because group will be loaded after all the components that
|
||||
# initialize devices have been setup.
|
||||
DEPENDENCIES = ['group', 'device_tracker', 'light']
|
||||
|
||||
# Configuration key for the entity id we are targetting
|
||||
# Configuration key for the entity id we are targeting.
|
||||
CONF_TARGET = 'target'
|
||||
|
||||
# Variable for storing configuration parameters
|
||||
# Variable for storing configuration parameters.
|
||||
TARGET_ID = None
|
||||
|
||||
# Name of the service that we expose
|
||||
# Name of the service that we expose.
|
||||
SERVICE_FLASH = 'flash'
|
||||
|
||||
# Shortcut for the logger
|
||||
@@ -59,16 +58,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup example component. """
|
||||
"""Setup example component."""
|
||||
global TARGET_ID
|
||||
|
||||
# Validate that all required config options are given
|
||||
# Validate that all required config options are given.
|
||||
if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER):
|
||||
return False
|
||||
|
||||
TARGET_ID = config[DOMAIN][CONF_TARGET]
|
||||
|
||||
# Validate that the target entity id exists
|
||||
# Validate that the target entity id exists.
|
||||
if hass.states.get(TARGET_ID) is None:
|
||||
_LOGGER.error("Target entity id %s does not exist",
|
||||
TARGET_ID)
|
||||
@@ -78,13 +77,13 @@ def setup(hass, config):
|
||||
TARGET_ID = None
|
||||
return False
|
||||
|
||||
# Tell the bootstrapper that we initialized successfully
|
||||
# Tell the bootstrapper that we initialized successfully.
|
||||
return True
|
||||
|
||||
|
||||
@track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES)
|
||||
def track_devices(hass, entity_id, old_state, new_state):
|
||||
""" Called when the group.all devices change state. """
|
||||
"""Called when the group.all devices change state."""
|
||||
# If the target id is not set, return
|
||||
if not TARGET_ID:
|
||||
return
|
||||
@@ -94,7 +93,7 @@ def track_devices(hass, entity_id, old_state, new_state):
|
||||
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
# If all people leave the house and the entity is on, turn it off
|
||||
# If all people leave the house and the entity is on, turn it off.
|
||||
elif new_state.state == STATE_NOT_HOME and core.is_on(hass, TARGET_ID):
|
||||
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
@@ -116,7 +115,7 @@ def wake_up(hass, now):
|
||||
|
||||
@track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF)
|
||||
def all_lights_off(hass, entity_id, old_state, new_state):
|
||||
""" If all lights turn off, turn off. """
|
||||
"""If all lights turn off, turn off."""
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
custom_components.hello_world
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Implements the bare minimum that a component should implement.
|
||||
The "hello world" custom component.
|
||||
|
||||
This component implements the bare minimum that a component should implement.
|
||||
|
||||
Configuration:
|
||||
|
||||
@@ -11,18 +11,18 @@ configuration.yaml file.
|
||||
hello_world:
|
||||
"""
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component
|
||||
# The domain of your component. Should be equal to the name of your component.
|
||||
DOMAIN = "hello_world"
|
||||
|
||||
# List of component names (string) your component depends upon
|
||||
# List of component names (string) your component depends upon.
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup our skeleton component. """
|
||||
"""Setup our skeleton component."""
|
||||
|
||||
# States are in the format DOMAIN.OBJECT_ID
|
||||
# States are in the format DOMAIN.OBJECT_ID.
|
||||
hass.states.set('hello_world.Hello_World', 'Works!')
|
||||
|
||||
# return boolean to indicate that initialization was successful
|
||||
# Return boolean to indicate that initialization was successfully.
|
||||
return True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
custom_components.mqtt_example
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Example of a custom MQTT component.
|
||||
|
||||
Shows how to communicate with MQTT. Follows a topic on MQTT and updates the
|
||||
state of an entity to the last message received on that topic.
|
||||
|
||||
@@ -15,45 +15,41 @@ configuration.yaml file.
|
||||
|
||||
mqtt_example:
|
||||
topic: home-assistant/mqtt_example
|
||||
|
||||
"""
|
||||
import homeassistant.loader as loader
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component
|
||||
# The domain of your component. Should be equal to the name of your component.
|
||||
DOMAIN = "mqtt_example"
|
||||
|
||||
# List of component names (string) your component depends upon
|
||||
# List of component names (string) your component depends upon.
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
||||
CONF_TOPIC = 'topic'
|
||||
DEFAULT_TOPIC = 'home-assistant/mqtt_example'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup our mqtt_example component. """
|
||||
"""Setup the MQTT example component."""
|
||||
mqtt = loader.get_component('mqtt')
|
||||
topic = config[DOMAIN].get('topic', DEFAULT_TOPIC)
|
||||
entity_id = 'mqtt_example.last_message'
|
||||
|
||||
# Listen to a message on MQTT
|
||||
|
||||
# Listen to a message on MQTT.
|
||||
def message_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
"""A new MQTT message has been received."""
|
||||
hass.states.set(entity_id, payload)
|
||||
|
||||
mqtt.subscribe(hass, topic, message_received)
|
||||
|
||||
hass.states.set(entity_id, 'No messages')
|
||||
|
||||
# Service to publish a message on MQTT
|
||||
|
||||
# Service to publish a message on MQTT.
|
||||
def set_state_service(call):
|
||||
""" Service to send a message. """
|
||||
"""Service to send a message."""
|
||||
mqtt.publish(hass, topic, call.data.get('new_state'))
|
||||
|
||||
# Register our service with Home Assistant
|
||||
# Register our service with Home Assistant.
|
||||
hass.services.register(DOMAIN, 'set_state', set_state_service)
|
||||
|
||||
# return boolean to indicate that initialization was successful
|
||||
# Return boolean to indicate that initialization was successfully.
|
||||
return True
|
||||
|
||||
+105
-35
@@ -1,13 +1,18 @@
|
||||
""" Starts home assistant. """
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from multiprocessing import Process
|
||||
|
||||
from homeassistant import bootstrap
|
||||
import homeassistant.config as config_util
|
||||
from homeassistant.const import __version__, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, RESTART_EXIT_CODE, __version__)
|
||||
|
||||
|
||||
def validate_python():
|
||||
@@ -73,6 +78,11 @@ def get_arguments():
|
||||
'--demo-mode',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in demo mode')
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in debug mode. Runs in single process to '
|
||||
'enable use of interactive debuggers.')
|
||||
parser.add_argument(
|
||||
'--open-ui',
|
||||
action='store_true',
|
||||
@@ -204,35 +214,11 @@ def uninstall_osx():
|
||||
print("Home Assistant has been uninstalled.")
|
||||
|
||||
|
||||
def main():
|
||||
""" Starts Home Assistant. """
|
||||
validate_python()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
config_dir = os.path.join(os.getcwd(), args.config)
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# os x launchd functions
|
||||
if args.install_osx:
|
||||
install_osx()
|
||||
return
|
||||
if args.uninstall_osx:
|
||||
uninstall_osx()
|
||||
return
|
||||
if args.restart_osx:
|
||||
uninstall_osx()
|
||||
install_osx()
|
||||
return
|
||||
|
||||
# daemon functions
|
||||
if args.pid_file:
|
||||
check_pid(args.pid_file)
|
||||
if args.daemon:
|
||||
daemonize()
|
||||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
def setup_and_run_hass(config_dir, args, top_process=False):
|
||||
"""
|
||||
Setup HASS and run. Block until stopped. Will assume it is running in a
|
||||
subprocess unless top_process is set to true.
|
||||
"""
|
||||
if args.demo_mode:
|
||||
config = {
|
||||
'frontend': {},
|
||||
@@ -259,7 +245,91 @@ def main():
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||
|
||||
hass.start()
|
||||
hass.block_till_stopped()
|
||||
exit_code = int(hass.block_till_stopped())
|
||||
|
||||
if not top_process:
|
||||
sys.exit(exit_code)
|
||||
return exit_code
|
||||
|
||||
|
||||
def run_hass_process(hass_proc):
|
||||
""" Runs a child hass process. Returns True if it should be restarted. """
|
||||
requested_stop = threading.Event()
|
||||
hass_proc.daemon = True
|
||||
|
||||
def request_stop(*args):
|
||||
""" request hass stop, *args is for signal handler callback """
|
||||
requested_stop.set()
|
||||
hass_proc.terminate()
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
except ValueError:
|
||||
print('Could not bind to SIGTERM. Are you running in a thread?')
|
||||
|
||||
hass_proc.start()
|
||||
try:
|
||||
hass_proc.join()
|
||||
except KeyboardInterrupt:
|
||||
request_stop()
|
||||
try:
|
||||
hass_proc.join()
|
||||
except KeyboardInterrupt:
|
||||
return False
|
||||
|
||||
return (not requested_stop.isSet() and
|
||||
hass_proc.exitcode == RESTART_EXIT_CODE,
|
||||
hass_proc.exitcode)
|
||||
|
||||
|
||||
def main():
|
||||
""" Starts Home Assistant. """
|
||||
validate_python()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
config_dir = os.path.join(os.getcwd(), args.config)
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# os x launchd functions
|
||||
if args.install_osx:
|
||||
install_osx()
|
||||
return 0
|
||||
if args.uninstall_osx:
|
||||
uninstall_osx()
|
||||
return 0
|
||||
if args.restart_osx:
|
||||
uninstall_osx()
|
||||
# A small delay is needed on some systems to let the unload finish.
|
||||
time.sleep(0.5)
|
||||
install_osx()
|
||||
return 0
|
||||
|
||||
# daemon functions
|
||||
if args.pid_file:
|
||||
check_pid(args.pid_file)
|
||||
if args.daemon:
|
||||
daemonize()
|
||||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
# Run hass in debug mode if requested
|
||||
if args.debug:
|
||||
sys.stderr.write('Running in debug mode. '
|
||||
'Home Assistant will not be able to restart.\n')
|
||||
exit_code = setup_and_run_hass(config_dir, args, top_process=True)
|
||||
if exit_code == RESTART_EXIT_CODE:
|
||||
sys.stderr.write('Home Assistant requested a '
|
||||
'restart in debug mode.\n')
|
||||
return exit_code
|
||||
|
||||
# Run hass as child process. Restart if necessary.
|
||||
keep_running = True
|
||||
while keep_running:
|
||||
hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args))
|
||||
keep_running, exit_code = run_hass_process(hass_proc)
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
||||
+57
-48
@@ -1,37 +1,31 @@
|
||||
"""
|
||||
homeassistant.bootstrap
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides methods to bootstrap a home assistant instance.
|
||||
"""Provides methods to bootstrap a home assistant instance."""
|
||||
|
||||
Each method will return a tuple (bus, statemachine).
|
||||
|
||||
After bootstrapping you can add your own components or
|
||||
start by calling homeassistant.start_home_assistant(bus)
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from threading import RLock
|
||||
|
||||
import homeassistant.core as core
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.util.package as pkg_util
|
||||
import homeassistant.util.location as loc_util
|
||||
import homeassistant.config as config_util
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.components as core_components
|
||||
import homeassistant.components.group as group
|
||||
import homeassistant.config as config_util
|
||||
import homeassistant.core as core
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.util.location as loc_util
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.const import (
|
||||
CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME,
|
||||
CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED,
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT, __version__)
|
||||
from homeassistant.helpers import event_decorators, service
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
__version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
|
||||
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_CUSTOMIZE,
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SETUP_LOCK = RLock()
|
||||
_CURRENT_SETUP = []
|
||||
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
@@ -78,42 +72,57 @@ def _handle_requirements(hass, component, name):
|
||||
|
||||
|
||||
def _setup_component(hass, domain, config):
|
||||
""" Setup a component for Home Assistant. """
|
||||
"""Setup a component for Home Assistant."""
|
||||
# pylint: disable=too-many-return-statements
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
component = loader.get_component(domain)
|
||||
|
||||
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
||||
if dep not in hass.config.components]
|
||||
with _SETUP_LOCK:
|
||||
# It might have been loaded while waiting for lock
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
|
||||
if missing_deps:
|
||||
_LOGGER.error(
|
||||
'Not initializing %s because not all dependencies loaded: %s',
|
||||
domain, ", ".join(missing_deps))
|
||||
return False
|
||||
|
||||
if not _handle_requirements(hass, component, domain):
|
||||
return False
|
||||
|
||||
try:
|
||||
if not component.setup(hass, config):
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
if domain in _CURRENT_SETUP:
|
||||
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
||||
domain, domain)
|
||||
return False
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error during setup of component %s', domain)
|
||||
return False
|
||||
|
||||
hass.config.components.append(component.DOMAIN)
|
||||
component = loader.get_component(domain)
|
||||
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
||||
if dep not in hass.config.components]
|
||||
|
||||
# Assumption: if a component does not depend on groups
|
||||
# it communicates with devices
|
||||
if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []):
|
||||
hass.pool.add_worker()
|
||||
if missing_deps:
|
||||
_LOGGER.error(
|
||||
'Not initializing %s because not all dependencies loaded: %s',
|
||||
domain, ", ".join(missing_deps))
|
||||
return False
|
||||
|
||||
hass.bus.fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN})
|
||||
if not _handle_requirements(hass, component, domain):
|
||||
return False
|
||||
|
||||
return True
|
||||
_CURRENT_SETUP.append(domain)
|
||||
|
||||
try:
|
||||
if not component.setup(hass, config):
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
return False
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error during setup of component %s', domain)
|
||||
return False
|
||||
finally:
|
||||
_CURRENT_SETUP.remove(domain)
|
||||
|
||||
hass.config.components.append(component.DOMAIN)
|
||||
|
||||
# Assumption: if a component does not depend on groups
|
||||
# it communicates with devices
|
||||
if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []):
|
||||
hass.pool.add_worker()
|
||||
|
||||
hass.bus.fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def prepare_setup_platform(hass, config, domain, platform_name):
|
||||
|
||||
@@ -21,7 +21,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
verisure.DISCOVER_SENSORS: 'verisure'
|
||||
verisure.DISCOVER_ALARMS: 'verisure'
|
||||
}
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
@@ -61,6 +61,8 @@ def setup(hass, config):
|
||||
|
||||
for alarm in target_alarms:
|
||||
getattr(alarm, method)(code)
|
||||
if alarm.should_poll:
|
||||
alarm.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@@ -9,18 +9,16 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom'
|
||||
'/archive/0.0.7.zip'
|
||||
'#pyalarmdotcom==0.0.7']
|
||||
'/archive/0.1.1.zip'
|
||||
'#pyalarmdotcom==0.1.1']
|
||||
DEFAULT_NAME = 'Alarm.com'
|
||||
|
||||
|
||||
@@ -57,10 +55,12 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
@@ -88,7 +88,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.disarm()
|
||||
self.update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
@@ -98,7 +97,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_stay()
|
||||
self.update_ha_state()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
@@ -108,7 +106,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_away()
|
||||
self.update_ha_state()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
""" Validate given code. """
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel.demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Demo platform that has two fake alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import homeassistant.components.alarm_control_panel.manual as manual
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Demo alarm control panels. """
|
||||
"""Setup the Demo alarm control panel platform."""
|
||||
add_devices([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10),
|
||||
])
|
||||
|
||||
@@ -6,15 +6,15 @@ Support for manual alarms.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.manual/
|
||||
"""
|
||||
import logging
|
||||
import datetime
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.mqtt/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel.nx584
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for NX584 alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.nx584/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Setup nx584. """
|
||||
host = config.get('host', 'localhost:5007')
|
||||
|
||||
try:
|
||||
add_devices([NX584Alarm(hass, host, config.get('name', 'NX584'))])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to NX584: %s', str(ex))
|
||||
return False
|
||||
|
||||
|
||||
class NX584Alarm(alarm.AlarmControlPanel):
|
||||
""" NX584-based alarm panel. """
|
||||
def __init__(self, hass, host, name):
|
||||
from nx584 import client
|
||||
self._hass = hass
|
||||
self._host = host
|
||||
self._name = name
|
||||
self._alarm = client.Client('http://%s' % host)
|
||||
# Do an initial list operation so that we will try to actually
|
||||
# talk to the API and trigger a requests exception for setup_platform()
|
||||
# to catch
|
||||
self._alarm.list_zones()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Polling needed. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" Characters if code is defined. """
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
zones = self._alarm.list_zones()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||
dict(host=self._host, reason=ex))
|
||||
return STATE_UNKNOWN
|
||||
except IndexError:
|
||||
_LOGGER.error('nx584 reports no partitions')
|
||||
return STATE_UNKNOWN
|
||||
|
||||
bypassed = False
|
||||
for zone in zones:
|
||||
if zone['bypassed']:
|
||||
_LOGGER.debug('Zone %(zone)s is bypassed, '
|
||||
'assuming HOME',
|
||||
dict(zone=zone['number']))
|
||||
bypassed = True
|
||||
break
|
||||
|
||||
if not part['armed']:
|
||||
return STATE_ALARM_DISARMED
|
||||
elif bypassed:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
self._alarm.disarm(code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
self._alarm.arm('home')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
self._alarm.arm('auto')
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
""" Alarm trigger command. """
|
||||
raise NotImplementedError()
|
||||
@@ -8,12 +8,12 @@ https://home-assistant.io/components/verisure/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.verisure as verisure
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,18 +21,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Verisure platform. """
|
||||
|
||||
if not verisure.MY_PAGES:
|
||||
_LOGGER.error('A connection has not been made to Verisure mypages.')
|
||||
return False
|
||||
|
||||
alarms = []
|
||||
|
||||
alarms.extend([
|
||||
VerisureAlarm(value)
|
||||
for value in verisure.ALARM_STATUS.values()
|
||||
if verisure.SHOW_ALARM
|
||||
])
|
||||
|
||||
if int(hub.config.get('alarm', '1')):
|
||||
hub.update_alarms()
|
||||
alarms.extend([
|
||||
VerisureAlarm(value.id)
|
||||
for value in hub.alarm_status.values()
|
||||
])
|
||||
add_devices(alarms)
|
||||
|
||||
|
||||
@@ -40,9 +35,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
""" Represents a Verisure alarm status. """
|
||||
|
||||
def __init__(self, alarm_status):
|
||||
self._id = alarm_status.id
|
||||
def __init__(self, device_id):
|
||||
self._id = device_id
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = int(hub.config.get('code_digits', '4'))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -56,41 +52,41 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" Four digit code required. """
|
||||
return '^\\d{%s}$' % verisure.CODE_DIGITS
|
||||
""" code format as regex """
|
||||
return '^\\d{%s}$' % self._digits
|
||||
|
||||
def update(self):
|
||||
""" Update alarm status """
|
||||
verisure.update_alarm()
|
||||
hub.update_alarms()
|
||||
|
||||
if verisure.ALARM_STATUS[self._id].status == 'unarmed':
|
||||
if hub.alarm_status[self._id].status == 'unarmed':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif verisure.ALARM_STATUS[self._id].status == 'armedhome':
|
||||
elif hub.alarm_status[self._id].status == 'armedhome':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif verisure.ALARM_STATUS[self._id].status == 'armed':
|
||||
elif hub.alarm_status[self._id].status == 'armed':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif verisure.ALARM_STATUS[self._id].status != 'pending':
|
||||
elif hub.alarm_status[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
'Unknown alarm state %s',
|
||||
verisure.ALARM_STATUS[self._id].status)
|
||||
hub.alarm_status[self._id].status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
verisure.MY_PAGES.alarm.set(code, 'DISARMED')
|
||||
hub.my_pages.alarm.set(code, 'DISARMED')
|
||||
_LOGGER.info('verisure alarm disarming')
|
||||
verisure.MY_PAGES.alarm.wait_while_pending()
|
||||
verisure.update_alarm()
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME')
|
||||
hub.my_pages.alarm.set(code, 'ARMED_HOME')
|
||||
_LOGGER.info('verisure alarm arming home')
|
||||
verisure.MY_PAGES.alarm.wait_while_pending()
|
||||
verisure.update_alarm()
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY')
|
||||
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
|
||||
_LOGGER.info('verisure alarm arming away')
|
||||
verisure.MY_PAGES.alarm.wait_while_pending()
|
||||
verisure.update_alarm()
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
components.alexa
|
||||
~~~~~~~~~~~~~~~~
|
||||
Component to offer a service end point for an Alexa skill.
|
||||
Support for Alexa skill service end point.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
@@ -10,8 +8,8 @@ import enum
|
||||
import logging
|
||||
|
||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||
from homeassistant.util import template
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
from homeassistant.helpers import template
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -28,7 +26,7 @@ CONF_ACTION = 'action'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Activate Alexa component. """
|
||||
"""Activate Alexa component."""
|
||||
_CONFIG.update(config[DOMAIN].get(CONF_INTENTS, {}))
|
||||
|
||||
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
|
||||
@@ -37,7 +35,7 @@ def setup(hass, config):
|
||||
|
||||
|
||||
def _handle_alexa(handler, path_match, data):
|
||||
""" Handle Alexa. """
|
||||
"""Handle Alexa."""
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
|
||||
req = data.get('request')
|
||||
@@ -99,19 +97,19 @@ def _handle_alexa(handler, path_match, data):
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
""" Alexa speech types. """
|
||||
"""Alexa speech types."""
|
||||
plaintext = "PlainText"
|
||||
ssml = "SSML"
|
||||
|
||||
|
||||
class CardType(enum.Enum):
|
||||
""" Alexa card types. """
|
||||
"""Alexa card types."""
|
||||
simple = "Simple"
|
||||
link_account = "LinkAccount"
|
||||
|
||||
|
||||
class AlexaResponse(object):
|
||||
""" Helps generating the response for Alexa. """
|
||||
"""Helps generating the response for Alexa."""
|
||||
|
||||
def __init__(self, hass, intent=None):
|
||||
self.hass = hass
|
||||
@@ -154,7 +152,7 @@ class AlexaResponse(object):
|
||||
}
|
||||
|
||||
def add_reprompt(self, speech_type, text):
|
||||
""" Add repromopt if user does not answer. """
|
||||
"""Add reprompt if user does not answer."""
|
||||
assert self.reprompt is None
|
||||
|
||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||
@@ -165,7 +163,7 @@ class AlexaResponse(object):
|
||||
}
|
||||
|
||||
def as_dict(self):
|
||||
""" Returns response in an Alexa valid dict. """
|
||||
"""Returns response in an Alexa valid dict."""
|
||||
response = {
|
||||
'shouldEndSession': self.should_end_session
|
||||
}
|
||||
@@ -188,5 +186,5 @@ class AlexaResponse(object):
|
||||
}
|
||||
|
||||
def _render(self, template_string):
|
||||
""" Render a response, adding data from intent if available. """
|
||||
"""Render a response, adding data from intent if available."""
|
||||
return template.render(self.hass, template_string, self.variables)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
homeassistant.components.apcupsd
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Sets up and provides access to the status output of APCUPSd via its Network
|
||||
Information Server (NIS).
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/apcupsd/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = "apcupsd"
|
||||
REQUIREMENTS = ("apcaccess==0.0.4",)
|
||||
|
||||
CONF_HOST = "host"
|
||||
CONF_PORT = "port"
|
||||
CONF_TYPE = "type"
|
||||
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_PORT = 3551
|
||||
|
||||
KEY_STATUS = "STATUS"
|
||||
|
||||
VALUE_ONLINE = "ONLINE"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
DATA = None
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Use config values to set up a function enabling status retrieval. """
|
||||
global DATA
|
||||
|
||||
host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST)
|
||||
port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT)
|
||||
|
||||
DATA = APCUPSdData(host, port)
|
||||
|
||||
# It doesn't really matter why we're not able to get the status, just that
|
||||
# we can't.
|
||||
# pylint: disable=broad-except
|
||||
try:
|
||||
DATA.update(no_throttle=True)
|
||||
except Exception:
|
||||
_LOGGER.exception("Failure while testing APCUPSd status retrieval.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class APCUPSdData(object):
|
||||
"""
|
||||
Stores the data retrieved from APCUPSd for each entity to use, acts as the
|
||||
single point responsible for fetching updates from the server.
|
||||
"""
|
||||
def __init__(self, host, port):
|
||||
from apcaccess import status
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._status = None
|
||||
self._get = status.get
|
||||
self._parse = status.parse
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
""" Get latest update if throttle allows. Return status. """
|
||||
self.update()
|
||||
return self._status
|
||||
|
||||
def _get_status(self):
|
||||
""" Get the status from APCUPSd and parse it into a dict. """
|
||||
return self._parse(self._get(host=self._host, port=self._port))
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self, **kwargs):
|
||||
"""
|
||||
Fetch the latest status from APCUPSd and store it in self._status.
|
||||
"""
|
||||
self._status = self._get_status()
|
||||
@@ -1,31 +1,27 @@
|
||||
"""
|
||||
homeassistant.components.api
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides a Rest API for Home Assistant.
|
||||
Rest API for Home Assistant.
|
||||
|
||||
For more details about the RESTful API, please refer to the documentation at
|
||||
https://home-assistant.io/developers/api/
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
import threading
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import TrackStates
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.util import template
|
||||
from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
||||
from homeassistant.const import (
|
||||
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
|
||||
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT,
|
||||
URL_API_TEMPLATE, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
|
||||
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
HTTP_UNPROCESSABLE_ENTITY, HTTP_HEADER_CONTENT_TYPE,
|
||||
CONTENT_TYPE_TEXT_PLAIN)
|
||||
|
||||
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
|
||||
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_ERROR_LOG, URL_API_EVENT_FORWARD, URL_API_EVENTS,
|
||||
URL_API_LOG_OUT, URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY,
|
||||
URL_API_STREAM, URL_API_TEMPLATE)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import TrackStates
|
||||
from homeassistant.helpers import template
|
||||
|
||||
DOMAIN = 'api'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -37,7 +33,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Register the API with the HTTP interface. """
|
||||
"""Register the API with the HTTP interface."""
|
||||
|
||||
# /api - for validation purposes
|
||||
hass.http.register_path('GET', URL_API, _handle_get_api)
|
||||
@@ -48,10 +44,6 @@ def setup(hass, config):
|
||||
# /api/config
|
||||
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
|
||||
|
||||
# /api/bootstrap
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
|
||||
|
||||
# /states
|
||||
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
|
||||
hass.http.register_path(
|
||||
@@ -63,6 +55,9 @@ def setup(hass, config):
|
||||
hass.http.register_path(
|
||||
'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_post_state_entity)
|
||||
hass.http.register_path(
|
||||
'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_delete_state_entity)
|
||||
|
||||
# /events
|
||||
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
|
||||
@@ -101,12 +96,12 @@ def setup(hass, config):
|
||||
|
||||
|
||||
def _handle_get_api(handler, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
"""Renders the debug interface."""
|
||||
handler.write_json_message("API running.")
|
||||
|
||||
|
||||
def _handle_get_api_stream(handler, path_match, data):
|
||||
""" Provide a streaming interface for the event bus. """
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
gracefully_closed = False
|
||||
hass = handler.server.hass
|
||||
wfile = handler.wfile
|
||||
@@ -119,7 +114,7 @@ def _handle_get_api_stream(handler, path_match, data):
|
||||
restrict = restrict.split(',')
|
||||
|
||||
def write_message(payload):
|
||||
""" Writes a message to the output. """
|
||||
"""Writes a message to the output."""
|
||||
with write_lock:
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
|
||||
@@ -132,7 +127,7 @@ def _handle_get_api_stream(handler, path_match, data):
|
||||
block.set()
|
||||
|
||||
def forward_events(event):
|
||||
""" Forwards events to the open request. """
|
||||
"""Forwards events to the open request."""
|
||||
nonlocal gracefully_closed
|
||||
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
|
||||
@@ -176,29 +171,17 @@ def _handle_get_api_stream(handler, path_match, data):
|
||||
|
||||
|
||||
def _handle_get_api_config(handler, path_match, data):
|
||||
""" Returns the Home Assistant config. """
|
||||
"""Returns the Home Assistant configuration."""
|
||||
handler.write_json(handler.server.hass.config.as_dict())
|
||||
|
||||
|
||||
def _handle_get_api_bootstrap(handler, path_match, data):
|
||||
""" Returns all data needed to bootstrap Home Assistant. """
|
||||
hass = handler.server.hass
|
||||
|
||||
handler.write_json({
|
||||
'config': hass.config.as_dict(),
|
||||
'states': hass.states.all(),
|
||||
'events': _events_json(hass),
|
||||
'services': _services_json(hass),
|
||||
})
|
||||
|
||||
|
||||
def _handle_get_api_states(handler, path_match, data):
|
||||
""" Returns a dict containing all entity ids and their state. """
|
||||
"""Returns a dict containing all entity ids and their state."""
|
||||
handler.write_json(handler.server.hass.states.all())
|
||||
|
||||
|
||||
def _handle_get_api_states_entity(handler, path_match, data):
|
||||
""" Returns the state of a specific entity. """
|
||||
"""Returns the state of a specific entity."""
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
state = handler.server.hass.states.get(entity_id)
|
||||
@@ -210,7 +193,7 @@ def _handle_get_api_states_entity(handler, path_match, data):
|
||||
|
||||
|
||||
def _handle_post_state_entity(handler, path_match, data):
|
||||
""" Handles updating the state of an entity.
|
||||
"""Handles updating the state of an entity.
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
@@ -240,13 +223,29 @@ def _handle_post_state_entity(handler, path_match, data):
|
||||
location=URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
|
||||
def _handle_delete_state_entity(handler, path_match, data):
|
||||
"""Handle request to delete an entity from state machine.
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
"""
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
if handler.server.hass.states.remove(entity_id):
|
||||
handler.write_json_message(
|
||||
"Entity not found", HTTP_NOT_FOUND)
|
||||
else:
|
||||
handler.write_json_message(
|
||||
"Entity removed", HTTP_OK)
|
||||
|
||||
|
||||
def _handle_get_api_events(handler, path_match, data):
|
||||
""" Handles getting overview of event listeners. """
|
||||
handler.write_json(_events_json(handler.server.hass))
|
||||
"""Handles getting overview of event listeners."""
|
||||
handler.write_json(events_json(handler.server.hass))
|
||||
|
||||
|
||||
def _handle_api_post_events_event(handler, path_match, event_data):
|
||||
""" Handles firing of an event.
|
||||
"""Handles firing of an event.
|
||||
|
||||
This handles the following paths:
|
||||
/api/events/<event_type>
|
||||
@@ -258,6 +257,7 @@ def _handle_api_post_events_event(handler, path_match, event_data):
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
handler.write_json_message(
|
||||
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
event_origin = ha.EventOrigin.remote
|
||||
|
||||
@@ -276,13 +276,13 @@ def _handle_api_post_events_event(handler, path_match, event_data):
|
||||
|
||||
|
||||
def _handle_get_api_services(handler, path_match, data):
|
||||
""" Handles getting overview of services. """
|
||||
handler.write_json(_services_json(handler.server.hass))
|
||||
"""Handles getting overview of services."""
|
||||
handler.write_json(services_json(handler.server.hass))
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_services_domain_service(handler, path_match, data):
|
||||
""" Handles calling a service.
|
||||
"""Handles calling a service.
|
||||
|
||||
This handles the following paths:
|
||||
/api/services/<domain>/<service>
|
||||
@@ -298,8 +298,7 @@ def _handle_post_api_services_domain_service(handler, path_match, data):
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_event_forward(handler, path_match, data):
|
||||
""" Handles adding an event forwarding target. """
|
||||
|
||||
"""Handles adding an event forwarding target."""
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
@@ -332,8 +331,7 @@ def _handle_post_api_event_forward(handler, path_match, data):
|
||||
|
||||
|
||||
def _handle_delete_api_event_forward(handler, path_match, data):
|
||||
""" Handles deleting an event forwarding target. """
|
||||
|
||||
"""Handles deleting an event forwarding target."""
|
||||
try:
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
@@ -356,26 +354,25 @@ def _handle_delete_api_event_forward(handler, path_match, data):
|
||||
|
||||
|
||||
def _handle_get_api_components(handler, path_match, data):
|
||||
""" Returns all the loaded components. """
|
||||
|
||||
"""Returns all the loaded components."""
|
||||
handler.write_json(handler.server.hass.config.components)
|
||||
|
||||
|
||||
def _handle_get_api_error_log(handler, path_match, data):
|
||||
""" Returns the logged errors for this session. """
|
||||
"""Returns the logged errors for this session."""
|
||||
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
|
||||
False)
|
||||
|
||||
|
||||
def _handle_post_api_log_out(handler, path_match, data):
|
||||
""" Log user out. """
|
||||
"""Log user out."""
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.destroy_session()
|
||||
handler.end_headers()
|
||||
|
||||
|
||||
def _handle_post_api_template(handler, path_match, data):
|
||||
""" Log user out. """
|
||||
"""Log user out."""
|
||||
template_string = data.get('template', '')
|
||||
|
||||
try:
|
||||
@@ -390,13 +387,13 @@ def _handle_post_api_template(handler, path_match, data):
|
||||
return
|
||||
|
||||
|
||||
def _services_json(hass):
|
||||
""" Generate services data to JSONify. """
|
||||
def services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
return [{"domain": key, "services": value}
|
||||
for key, value in hass.services.services.items()]
|
||||
|
||||
|
||||
def _events_json(hass):
|
||||
""" Generate event data to JSONify. """
|
||||
def events_json(hass):
|
||||
"""Generate event data to JSONify."""
|
||||
return [{"event": key, "listener_count": value}
|
||||
for key, value in hass.bus.listeners.items()]
|
||||
|
||||
@@ -9,9 +9,9 @@ https://home-assistant.io/components/arduino/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
DOMAIN = "arduino"
|
||||
REQUIREMENTS = ['PyMata==2.07a']
|
||||
|
||||
@@ -6,13 +6,12 @@ Offers 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
|
||||
"""
|
||||
from functools import partial
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.util import template
|
||||
|
||||
from homeassistant.helpers import template
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_BELOW = "below"
|
||||
|
||||
@@ -7,15 +7,47 @@ For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#state-trigger
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.const import MATCH_ALL
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL)
|
||||
from homeassistant.components.automation.time import (
|
||||
CONF_HOURS, CONF_MINUTES, CONF_SECONDS)
|
||||
from homeassistant.helpers.event import track_state_change, track_point_in_time
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_FROM = "from"
|
||||
CONF_TO = "to"
|
||||
CONF_STATE = "state"
|
||||
CONF_FOR = "for"
|
||||
|
||||
|
||||
def get_time_config(config):
|
||||
""" Helper function to extract the time specified in the config """
|
||||
if CONF_FOR not in config:
|
||||
return None
|
||||
|
||||
hours = config[CONF_FOR].get(CONF_HOURS)
|
||||
minutes = config[CONF_FOR].get(CONF_MINUTES)
|
||||
seconds = config[CONF_FOR].get(CONF_SECONDS)
|
||||
|
||||
if hours is None and minutes is None and seconds is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Received invalid value for '%s': %s",
|
||||
config[CONF_FOR], CONF_FOR)
|
||||
return None
|
||||
|
||||
if config.get(CONF_TO) is None and config.get(CONF_STATE) is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"For: requires a to: value'%s': %s",
|
||||
config[CONF_FOR], CONF_FOR)
|
||||
return None
|
||||
|
||||
return timedelta(hours=(hours or 0.0),
|
||||
minutes=(minutes or 0.0),
|
||||
seconds=(seconds or 0.0))
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
@@ -25,19 +57,47 @@ def trigger(hass, config, action):
|
||||
if entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing trigger configuration key %s", CONF_ENTITY_ID)
|
||||
return False
|
||||
return None
|
||||
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
||||
time_delta = get_time_config(config)
|
||||
|
||||
if isinstance(from_state, bool) or isinstance(to_state, bool):
|
||||
logging.getLogger(__name__).error(
|
||||
'Config error. Surround to/from values with quotes.')
|
||||
return False
|
||||
return None
|
||||
|
||||
if CONF_FOR in config and time_delta is None:
|
||||
return None
|
||||
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
action()
|
||||
|
||||
def state_for_listener(now):
|
||||
""" Fires on state changes after a delay and calls action. """
|
||||
hass.bus.remove_listener(
|
||||
EVENT_STATE_CHANGED, for_state_listener)
|
||||
action()
|
||||
|
||||
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
||||
""" Fires on state changes and cancels
|
||||
for listener if state changed. """
|
||||
if inner_to_s == to_s:
|
||||
return
|
||||
hass.bus.remove_listener(EVENT_TIME_CHANGED, for_time_listener)
|
||||
hass.bus.remove_listener(
|
||||
EVENT_STATE_CHANGED, for_state_listener)
|
||||
|
||||
if time_delta is not None:
|
||||
target_tm = dt_util.utcnow() + time_delta
|
||||
for_time_listener = track_point_in_time(
|
||||
hass, state_for_listener, target_tm)
|
||||
for_state_listener = track_state_change(
|
||||
hass, entity_id, state_for_cancel_listener,
|
||||
MATCH_ALL, MATCH_ALL)
|
||||
else:
|
||||
action()
|
||||
|
||||
track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
@@ -56,10 +116,18 @@ def if_action(hass, config):
|
||||
CONF_STATE)
|
||||
return None
|
||||
|
||||
time_delta = get_time_config(config)
|
||||
if CONF_FOR in config and time_delta is None:
|
||||
return None
|
||||
|
||||
state = str(state)
|
||||
|
||||
def if_state():
|
||||
""" Test if condition. """
|
||||
return hass.states.is_state(entity_id, state)
|
||||
is_state = hass.states.is_state(entity_id, state)
|
||||
return (time_delta is None and is_state or
|
||||
time_delta is not None and
|
||||
dt_util.utcnow() - time_delta >
|
||||
hass.states.get(entity_id).last_changed)
|
||||
|
||||
return if_state
|
||||
|
||||
@@ -9,9 +9,9 @@ at https://home-assistant.io/components/automation/#sun-trigger
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import sun
|
||||
from homeassistant.helpers.event import track_sunrise, track_sunset
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
DEPENDENCIES = ['sun']
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.util import template
|
||||
from homeassistant.helpers import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -9,10 +9,9 @@ at https://home-assistant.io/components/automation/#zone-trigger
|
||||
import logging
|
||||
|
||||
from homeassistant.components import zone
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL)
|
||||
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_ZONE = "zone"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with binary sensors (sensors which only know two states)
|
||||
that can be monitored.
|
||||
|
||||
@@ -12,17 +10,43 @@ import logging
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF)
|
||||
from homeassistant.components import (bloomsky, mysensors, zwave, wink)
|
||||
|
||||
DOMAIN = 'binary_sensor'
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
SENSOR_CLASSES = [
|
||||
None, # Generic on/off
|
||||
'opening', # Door, window, etc
|
||||
'motion', # Motion sensor
|
||||
'gas', # CO, CO2, etc
|
||||
'smoke', # Smoke detector
|
||||
'moisture', # Specifically a wetness sensor
|
||||
'light', # Lightness threshold
|
||||
'power', # Power, over-current, etc
|
||||
'safety', # Generic on=unsafe, off=safe
|
||||
'heat', # On means hot (or too hot)
|
||||
'cold', # On means cold (or too cold)
|
||||
'moving', # On means moving, Off means stopped
|
||||
'sound', # On means sound detected, Off means no sound
|
||||
'vibration', # On means vibration detected, Off means no vibration
|
||||
]
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
bloomsky.DISCOVER_BINARY_SENSORS: 'bloomsky',
|
||||
mysensors.DISCOVER_BINARY_SENSORS: 'mysensors',
|
||||
zwave.DISCOVER_BINARY_SENSORS: 'zwave',
|
||||
wink.DISCOVER_BINARY_SENSORS: 'wink'
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for binary sensors. """
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
@@ -31,19 +55,29 @@ def setup(hass, config):
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class BinarySensorDevice(Entity):
|
||||
""" Represents a binary sensor. """
|
||||
"""Represent a binary sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
"""Return True if the binary sensor is on."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the binary sensor. """
|
||||
"""Return the state of the binary sensor."""
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def friendly_state(self):
|
||||
""" Returns the friendly state of the binary sensor. """
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_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,42 @@
|
||||
"""
|
||||
Provides a binary sensor to track online status of a UPS.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.apcupsd/
|
||||
"""
|
||||
from homeassistant.components import apcupsd
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = [apcupsd.DOMAIN]
|
||||
DEFAULT_NAME = "UPS Online Status"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Instantiate an OnlineStatus binary sensor entity."""
|
||||
add_entities((OnlineStatus(config, apcupsd.DATA),))
|
||||
|
||||
|
||||
class OnlineStatus(BinarySensorDevice):
|
||||
"""Binary sensor to represent UPS online status."""
|
||||
def __init__(self, config, data):
|
||||
self._config = config
|
||||
self._data = data
|
||||
self._state = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the UPS online status sensor. """
|
||||
return self._config.get("name", DEFAULT_NAME)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the UPS is online, else False."""
|
||||
return self._state == apcupsd.VALUE_ONLINE
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Get the status report from APCUPSd (or cache) and set this entity's
|
||||
state.
|
||||
"""
|
||||
self._state = self._data.status[apcupsd.KEY_STATUS]
|
||||
@@ -1,18 +1,16 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.arest
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The arest sensor will consume an exposed aREST API of a device.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.arest/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,8 +22,7 @@ CONF_PIN = 'pin'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the aREST binary sensor. """
|
||||
|
||||
"""Get the aREST binary sensor."""
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
pin = config.get(CONF_PIN)
|
||||
|
||||
@@ -56,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
""" Implements an aREST binary sensor for a pin. """
|
||||
"""Implements an aREST binary sensor for a pin."""
|
||||
|
||||
def __init__(self, arest, resource, name, pin):
|
||||
self.arest = arest
|
||||
@@ -73,23 +70,22 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the binary sensor. """
|
||||
"""The name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
"""True if the binary sensor is on."""
|
||||
return bool(self.arest.data.get('state'))
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST API. """
|
||||
"""Gets the latest data from aREST API."""
|
||||
self.arest.update()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ArestData(object):
|
||||
""" Class for handling the data retrieval for pins. """
|
||||
|
||||
"""Class for handling the data retrieval for pins."""
|
||||
def __init__(self, resource, pin):
|
||||
self._resource = resource
|
||||
self._pin = pin
|
||||
@@ -97,7 +93,7 @@ class ArestData(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from aREST device. """
|
||||
"""Gets the latest data from aREST device."""
|
||||
try:
|
||||
response = requests.get('{}/digital/{}'.format(
|
||||
self._resource, self._pin), timeout=10)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Support the binary sensors of a BloomSky weather station.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.bloomsky/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ["bloomsky"]
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
"Rain": "moisture",
|
||||
"Night": None,
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the available BloomSky weather binary sensors."""
|
||||
logger = logging.getLogger(__name__)
|
||||
bloomsky = get_component('bloomsky')
|
||||
sensors = config.get('monitored_conditions', SENSOR_TYPES)
|
||||
|
||||
for device in bloomsky.BLOOMSKY.devices.values():
|
||||
for variable in sensors:
|
||||
if variable in SENSOR_TYPES:
|
||||
add_devices([BloomSkySensor(bloomsky.BLOOMSKY,
|
||||
device,
|
||||
variable)])
|
||||
else:
|
||||
logger.error("Cannot find definition for device: %s", variable)
|
||||
|
||||
|
||||
class BloomSkySensor(BinarySensorDevice):
|
||||
""" Represents a single binary sensor in a BloomSky device. """
|
||||
|
||||
def __init__(self, bs, device, sensor_name):
|
||||
"""Initialize a BloomSky binary sensor."""
|
||||
self._bloomsky = bs
|
||||
self._device_id = device["DeviceID"]
|
||||
self._sensor_name = sensor_name
|
||||
self._name = "{} {}".format(device["DeviceName"], sensor_name)
|
||||
self._unique_id = "bloomsky_binary_sensor {}".format(self._name)
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the BloomSky device and this sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""If binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Request an update from the BloomSky API."""
|
||||
self._bloomsky.refresh_devices()
|
||||
|
||||
self._state = \
|
||||
self._bloomsky.devices[self._device_id]["Data"][self._sensor_name]
|
||||
+15
-14
@@ -1,16 +1,17 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.command_sensor
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure custom shell commands to turn a value
|
||||
into a logical value for a binary sensor.
|
||||
Allows to configure custom shell commands to turn a value into a logical value
|
||||
for a binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.command/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.command_sensor import CommandSensorData
|
||||
from homeassistant.util import template
|
||||
from homeassistant.components.sensor.command_line import CommandSensorData
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.helpers import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,8 +25,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Add the Command Sensor. """
|
||||
|
||||
"""Add the Command Sensor."""
|
||||
if config.get('command') is None:
|
||||
_LOGGER.error('Missing required variable: "command"')
|
||||
return False
|
||||
@@ -44,8 +44,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class CommandBinarySensor(BinarySensorDevice):
|
||||
""" Represents a binary sensor that is returning
|
||||
a value of a shell commands. """
|
||||
"""
|
||||
Represents a binary sensor that is returning a value of a shell commands.
|
||||
"""
|
||||
def __init__(self, hass, data, name, payload_on,
|
||||
payload_off, value_template):
|
||||
self._hass = hass
|
||||
@@ -59,16 +60,16 @@ class CommandBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
"""The name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
"""True if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data and updates the state. """
|
||||
"""Gets the latest data and updates the state."""
|
||||
self.data.update()
|
||||
value = self.data.value
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Demo platform that has two fake binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Demo binary sensors. """
|
||||
"""Setup the Demo binary sensor platform."""
|
||||
add_devices([
|
||||
DemoBinarySensor('Basement Floor Wet', False),
|
||||
DemoBinarySensor('Movement Backyard', True),
|
||||
DemoBinarySensor('Basement Floor Wet', False, 'moisture'),
|
||||
DemoBinarySensor('Movement Backyard', True, 'motion'),
|
||||
])
|
||||
|
||||
|
||||
class DemoBinarySensor(BinarySensorDevice):
|
||||
""" A Demo binary sensor. """
|
||||
|
||||
def __init__(self, name, state):
|
||||
"""A Demo binary sensor."""
|
||||
def __init__(self, name, state, sensor_class):
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._sensor_type = sensor_class
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a demo binary sensor. """
|
||||
"""No polling needed for a demo binary sensor."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the binary sensor. """
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure a MQTT binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
@@ -8,10 +6,10 @@ https://home-assistant.io/components/binary_sensor.mqtt/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.util import template
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.helpers import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,7 +23,7 @@ DEPENDENCIES = ['mqtt']
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Add MQTT binary sensor. """
|
||||
"""Add MQTT binary sensor."""
|
||||
|
||||
if config.get('state_topic') is None:
|
||||
_LOGGER.error('Missing required variable: state_topic')
|
||||
@@ -43,7 +41,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttBinarySensor(BinarySensorDevice):
|
||||
""" Represents a binary sensor that is updated by MQTT. """
|
||||
"""Represents a binary sensor that is updated by MQTT."""
|
||||
def __init__(self, hass, name, state_topic, qos, payload_on, payload_off,
|
||||
value_template):
|
||||
self._hass = hass
|
||||
@@ -55,7 +53,7 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
self._qos = qos
|
||||
|
||||
def message_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = template.render_with_possible_json_value(
|
||||
hass, value_template, payload)
|
||||
@@ -70,15 +68,15 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the binary sensor. """
|
||||
"""The name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
"""True if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Support for MySensors binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mysensors/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES)
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for sensors."""
|
||||
# Only act if loaded via mysensors by discovery event.
|
||||
# Otherwise gateway is not setup.
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
mysensors = get_component('mysensors')
|
||||
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||
# states. Map them in a dict of lists.
|
||||
pres = gateway.const.Presentation
|
||||
set_req = gateway.const.SetReq
|
||||
map_sv_types = {
|
||||
pres.S_DOOR: [set_req.V_TRIPPED],
|
||||
pres.S_MOTION: [set_req.V_TRIPPED],
|
||||
pres.S_SMOKE: [set_req.V_TRIPPED],
|
||||
}
|
||||
if float(gateway.version) >= 1.5:
|
||||
map_sv_types.update({
|
||||
pres.S_SPRINKLER: [set_req.V_TRIPPED],
|
||||
pres.S_WATER_LEAK: [set_req.V_TRIPPED],
|
||||
pres.S_SOUND: [set_req.V_TRIPPED],
|
||||
pres.S_VIBRATION: [set_req.V_TRIPPED],
|
||||
pres.S_MOISTURE: [set_req.V_TRIPPED],
|
||||
})
|
||||
|
||||
devices = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
map_sv_types, devices, add_devices, MySensorsBinarySensor))
|
||||
|
||||
|
||||
class MySensorsBinarySensor(BinarySensorDevice):
|
||||
"""Represent the value of a MySensors child node."""
|
||||
|
||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||
|
||||
def __init__(
|
||||
self, gateway, node_id, child_id, name, value_type, child_type):
|
||||
"""
|
||||
Setup class attributes on instantiation.
|
||||
|
||||
Args:
|
||||
gateway (GatewayWrapper): Gateway object.
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
child_type (str): Child type of child.
|
||||
|
||||
Attributes:
|
||||
gateway (GatewayWrapper): Gateway object.
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
_name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
child_type (str): Child type of child.
|
||||
battery_level (int): Node battery level.
|
||||
_values (dict): Child values. Non state values set as state attributes.
|
||||
mysensors (module): Mysensors main component module.
|
||||
"""
|
||||
self.gateway = gateway
|
||||
self.node_id = node_id
|
||||
self.child_id = child_id
|
||||
self._name = name
|
||||
self.value_type = value_type
|
||||
self.child_type = child_type
|
||||
self.battery_level = 0
|
||||
self._values = {}
|
||||
self.mysensors = get_component('mysensors')
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""MySensor gateway pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of this entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {
|
||||
self.mysensors.ATTR_PORT: self.gateway.port,
|
||||
self.mysensors.ATTR_NODE_ID: self.node_id,
|
||||
self.mysensors.ATTR_CHILD_ID: self.child_id,
|
||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||
}
|
||||
|
||||
set_req = self.gateway.const.SetReq
|
||||
|
||||
for value_type, value in self._values.items():
|
||||
if value_type != self.value_type:
|
||||
try:
|
||||
attr[set_req(value_type).name] = value
|
||||
except ValueError:
|
||||
_LOGGER.error('value_type %s is not valid for mysensors '
|
||||
'version %s', value_type,
|
||||
self.gateway.version)
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
if self.value_type in self._values:
|
||||
return self._values[self.value_type] == STATE_ON
|
||||
return False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
pres = self.gateway.const.Presentation
|
||||
class_map = {
|
||||
pres.S_DOOR: 'opening',
|
||||
pres.S_MOTION: 'motion',
|
||||
pres.S_SMOKE: 'smoke',
|
||||
}
|
||||
if float(self.gateway.version) >= 1.5:
|
||||
class_map.update({
|
||||
pres.S_SPRINKLER: 'sprinkler',
|
||||
pres.S_WATER_LEAK: 'leak',
|
||||
pres.S_SOUND: 'sound',
|
||||
pres.S_VIBRATION: 'vibration',
|
||||
pres.S_MOISTURE: 'moisture',
|
||||
})
|
||||
if class_map.get(self.child_type) in SENSOR_CLASSES:
|
||||
return class_map.get(self.child_type)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.value_type in self._values
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest values from a sensor."""
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
child = node.children[self.child_id]
|
||||
for value_type, value in child.values.items():
|
||||
_LOGGER.debug(
|
||||
"%s: value_type %s, value = %s", self._name, value_type, value)
|
||||
if value_type == self.gateway.const.SetReq.V_TRIPPED:
|
||||
self._values[value_type] = STATE_ON if int(
|
||||
value) == 1 else STATE_OFF
|
||||
else:
|
||||
self._values[value_type] = value
|
||||
|
||||
self.battery_level = node.battery_level
|
||||
@@ -1,6 +1,4 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.nest
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Nest Thermostat Binary Sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
@@ -8,15 +6,16 @@ https://home-assistant.io/components/binary_sensor.nest/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import homeassistant.components.nest as nest
|
||||
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
BINARY_TYPES = ['fan',
|
||||
'hvac_ac_state',
|
||||
'hvac_aux_heater_state',
|
||||
'hvac_heater_state',
|
||||
'hvac_heat_x2_state',
|
||||
'hvac_heat_x3_state',
|
||||
'hvac_alt_heat_state',
|
||||
@@ -26,7 +25,7 @@ BINARY_TYPES = ['fan',
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Setup Nest binary sensors. """
|
||||
"""Setup Nest binary sensors."""
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
@@ -47,9 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
""" Represents a Nest binary sensor. """
|
||||
"""Represents a Nest binary sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
"""True if the binary sensor is on."""
|
||||
return bool(getattr(self.device, self.variable))
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Support for exposing nx584 elements as sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.nx584/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
SENSOR_CLASSES, BinarySensorDevice)
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.2']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup nx584 sensors."""
|
||||
from nx584 import client as nx584_client
|
||||
|
||||
host = config.get('host', 'localhost:5007')
|
||||
exclude = config.get('exclude_zones', [])
|
||||
zone_types = config.get('zone_types', {})
|
||||
|
||||
if not all(isinstance(zone, int) for zone in exclude):
|
||||
_LOGGER.error('Invalid excluded zone specified (use zone number)')
|
||||
return False
|
||||
|
||||
if not all(isinstance(zone, int) and ztype in SENSOR_CLASSES
|
||||
for zone, ztype in zone_types.items()):
|
||||
_LOGGER.error('Invalid zone_types entry')
|
||||
return False
|
||||
|
||||
try:
|
||||
client = nx584_client.Client('http://%s' % host)
|
||||
zones = client.list_zones()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to NX584: %s', str(ex))
|
||||
return False
|
||||
|
||||
version = [int(v) for v in client.get_version().split('.')]
|
||||
if version < [1, 1]:
|
||||
_LOGGER.error('NX584 is too old to use for sensors (>=0.2 required)')
|
||||
return False
|
||||
|
||||
zone_sensors = {
|
||||
zone['number']: NX584ZoneSensor(
|
||||
zone,
|
||||
zone_types.get(zone['number'], 'opening'))
|
||||
for zone in zones
|
||||
if zone['number'] not in exclude}
|
||||
if zone_sensors:
|
||||
add_devices(zone_sensors.values())
|
||||
watcher = NX584Watcher(client, zone_sensors)
|
||||
watcher.start()
|
||||
else:
|
||||
_LOGGER.warning('No zones found on NX584')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class NX584ZoneSensor(BinarySensorDevice):
|
||||
"""Represents a NX584 zone as a sensor."""
|
||||
|
||||
def __init__(self, zone, zone_type):
|
||||
self._zone = zone
|
||||
self._zone_type = zone_type
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the binary sensor."""
|
||||
return self._zone['name']
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
# True means "faulted" or "open" or "abnormal state"
|
||||
return self._zone['state']
|
||||
|
||||
|
||||
class NX584Watcher(threading.Thread):
|
||||
"""Event listener thread to process NX584 events."""
|
||||
|
||||
def __init__(self, client, zone_sensors):
|
||||
super(NX584Watcher, self).__init__()
|
||||
self.daemon = True
|
||||
self._client = client
|
||||
self._zone_sensors = zone_sensors
|
||||
|
||||
def _process_zone_event(self, event):
|
||||
zone = event['zone']
|
||||
zone_sensor = self._zone_sensors.get(zone)
|
||||
# pylint: disable=protected-access
|
||||
if not zone_sensor:
|
||||
return
|
||||
zone_sensor._zone['state'] = event['zone_state']
|
||||
zone_sensor.update_ha_state()
|
||||
|
||||
def _process_events(self, events):
|
||||
for event in events:
|
||||
if event.get('type') == 'zone_status':
|
||||
self._process_zone_event(event)
|
||||
|
||||
def _run(self):
|
||||
# Throw away any existing events so we don't replay history
|
||||
self._client.get_events()
|
||||
while True:
|
||||
events = self._client.get_events()
|
||||
if events:
|
||||
self._process_events(events)
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
self._run()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('Failed to reach NX584 server')
|
||||
time.sleep(10)
|
||||
@@ -1,6 +1,4 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.rest
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The rest binary sensor will consume responses sent by an exposed REST API.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
@@ -8,10 +6,10 @@ https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.util import template
|
||||
from homeassistant.components.sensor.rest import RestData
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.rest import RestData
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.helpers import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
"""REST binary sensor."""
|
||||
"""A REST binary sensor."""
|
||||
|
||||
def __init__(self, hass, rest, name, value_template):
|
||||
"""Initialize a REST binary sensor."""
|
||||
@@ -59,7 +57,7 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if the binary sensor is on."""
|
||||
"""Return true if the binary sensor is on."""
|
||||
if self.rest.data is None:
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.rpi_gpio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure a binary sensor using RPi GPIO.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rpi_gpio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import (DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
|
||||
DEFAULT_PULL_MODE = "UP"
|
||||
DEFAULT_BOUNCETIME = 50
|
||||
@@ -22,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Raspberry PI GPIO devices. """
|
||||
"""Sets up the Raspberry PI GPIO devices."""
|
||||
|
||||
pull_mode = config.get('pull_mode', DEFAULT_PULL_MODE)
|
||||
bouncetime = config.get('bouncetime', DEFAULT_BOUNCETIME)
|
||||
@@ -38,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
""" Represents a binary sensor that uses Raspberry Pi GPIO. """
|
||||
"""Represents a binary sensor that uses Raspberry Pi GPIO."""
|
||||
def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
|
||||
# pylint: disable=no-member
|
||||
|
||||
@@ -52,22 +50,22 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
|
||||
def read_gpio(port):
|
||||
""" Reads state from GPIO. """
|
||||
"""Reads state from GPIO."""
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
self.update_ha_state()
|
||||
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
"""The name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" Returns the state of the entity. """
|
||||
"""Returns the state of the entity."""
|
||||
return self._state != self._invert_logic
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Provides a binary sensor which gets its values from a TCP socket.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.tcp/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.tcp import Sensor, DOMAIN, CONF_VALUE_ON
|
||||
|
||||
DEPENDENCIES = [DOMAIN]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Create the binary sensor."""
|
||||
if not BinarySensor.validate_config(config):
|
||||
return False
|
||||
|
||||
add_entities((BinarySensor(hass, config),))
|
||||
|
||||
|
||||
class BinarySensor(BinarySensorDevice, Sensor):
|
||||
"""A binary sensor which is on when its state == CONF_VALUE_ON."""
|
||||
required = (CONF_VALUE_ON,)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the binary sensor is on."""
|
||||
return self._state == self._config[CONF_VALUE_ON]
|
||||
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.template
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for exposing a templated binary_sensor
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
DOMAIN,
|
||||
SENSOR_CLASSES)
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE
|
||||
from homeassistant.core import EVENT_STATE_CHANGED
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.util import slugify
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
CONF_SENSORS = 'sensors'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup template binary sensors."""
|
||||
|
||||
sensors = []
|
||||
if config.get(CONF_SENSORS) is None:
|
||||
_LOGGER.error('Missing configuration data for binary_sensor platform')
|
||||
return False
|
||||
|
||||
for device, device_config in config[CONF_SENSORS].items():
|
||||
|
||||
if device != slugify(device):
|
||||
_LOGGER.error('Found invalid key for binary_sensor.template: %s. '
|
||||
'Use %s instead', device, slugify(device))
|
||||
continue
|
||||
|
||||
if not isinstance(device_config, dict):
|
||||
_LOGGER.error('Missing configuration data for binary_sensor %s',
|
||||
device)
|
||||
continue
|
||||
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||
sensor_class = device_config.get('sensor_class')
|
||||
value_template = device_config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
if sensor_class not in SENSOR_CLASSES:
|
||||
_LOGGER.error('Sensor class is not valid')
|
||||
continue
|
||||
|
||||
if value_template is None:
|
||||
_LOGGER.error(
|
||||
'Missing %s for sensor %s', CONF_VALUE_TEMPLATE, device)
|
||||
continue
|
||||
|
||||
sensors.append(
|
||||
BinarySensorTemplate(
|
||||
hass,
|
||||
device,
|
||||
friendly_name,
|
||||
sensor_class,
|
||||
value_template)
|
||||
)
|
||||
if not sensors:
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
add_devices(sensors)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BinarySensorTemplate(BinarySensorDevice):
|
||||
"""A virtual binary_sensor that triggers from another sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, device, friendly_name, sensor_class,
|
||||
value_template):
|
||||
self._hass = hass
|
||||
self._device = device
|
||||
self._name = friendly_name
|
||||
self._sensor_class = sensor_class
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
|
||||
self.entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, device,
|
||||
hass=hass)
|
||||
|
||||
_LOGGER.info('Started template sensor %s', device)
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
|
||||
|
||||
def _event_listener(self, event):
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
return self._sensor_class
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
value = template.render(self._hass, self._template)
|
||||
except TemplateError as ex:
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
# Common during HA startup - so just a warning
|
||||
_LOGGER.warning(ex)
|
||||
return
|
||||
_LOGGER.error(ex)
|
||||
value = 'false'
|
||||
self._state = value.lower() == 'true'
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Support for Wink sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/sensor.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.6.2']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
"opened": "opening",
|
||||
"brightness": "light",
|
||||
"vibration": "vibration",
|
||||
"loudness": "sound"
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Sets up the Wink platform."""
|
||||
import pywink
|
||||
|
||||
if discovery_info is None:
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if token is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing wink access_token. "
|
||||
"Get one at https://winkbearertoken.appspot.com/")
|
||||
return
|
||||
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
for sensor in pywink.get_sensors():
|
||||
if sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
||||
"""Represents a Wink sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
self.wink = wink
|
||||
self._unit_of_measurement = self.wink.UNIT
|
||||
self.capability = self.wink.capability()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
if self.capability == "loudness":
|
||||
return self.wink.loudness_boolean()
|
||||
elif self.capability == "vibration":
|
||||
return self.wink.vibration_boolean()
|
||||
elif self.capability == "brightness":
|
||||
return self.wink.brightness_boolean()
|
||||
else:
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this wink sensor """
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the sensor if any. """
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
""" Update state of the sensor. """
|
||||
self.wink.update_state()
|
||||
@@ -1,21 +1,18 @@
|
||||
"""
|
||||
homeassistant.components.binary_sensor.zigbee
|
||||
|
||||
Contains functionality to use a ZigBee device as a binary sensor.
|
||||
"""
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.zigbee/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.zigbee import (
|
||||
ZigBeeDigitalIn, ZigBeeDigitalInConfig)
|
||||
|
||||
|
||||
DEPENDENCIES = ["zigbee"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""
|
||||
Create and add an entity based on the configuration.
|
||||
"""
|
||||
"""Create and add an entity based on the configuration."""
|
||||
add_entities([
|
||||
ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))
|
||||
])
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Interfaces with Z-Wave sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/binary_sensor.zwave/
|
||||
"""
|
||||
import logging
|
||||
import datetime
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
from homeassistant.components.zwave import (
|
||||
ATTR_NODE_ID, ATTR_VALUE_ID,
|
||||
COMMAND_CLASS_SENSOR_BINARY, NETWORK,
|
||||
ZWaveDeviceEntity, get_config_value)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
BinarySensorDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
PHILIO = 0x013c
|
||||
PHILIO_SLIM_SENSOR = 0x0002
|
||||
PHILIO_SLIM_SENSOR_MOTION = (PHILIO, PHILIO_SLIM_SENSOR, 0)
|
||||
|
||||
WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Z-Wave platform for sensors."""
|
||||
|
||||
if discovery_info is None or NETWORK is None:
|
||||
return
|
||||
|
||||
node = NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
||||
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16),
|
||||
value.index)
|
||||
|
||||
value.set_change_verified(False)
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_NO_OFF_EVENT:
|
||||
# Default the multiplier to 4
|
||||
re_arm_multiplier = (get_config_value(value.node, 9) or 4)
|
||||
add_devices([
|
||||
ZWaveTriggerSensor(value, "motion",
|
||||
hass, re_arm_multiplier * 8)
|
||||
])
|
||||
|
||||
elif value.command_class == COMMAND_CLASS_SENSOR_BINARY:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
|
||||
|
||||
class ZWaveBinarySensor(BinarySensorDevice, ZWaveDeviceEntity):
|
||||
"""Represents a binary sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, sensor_class):
|
||||
self._sensor_type = sensor_class
|
||||
# pylint: disable=import-error
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._value.data
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state()
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
"""
|
||||
Represents a stateless sensor which triggers events just 'On'
|
||||
within Z-Wave.
|
||||
"""
|
||||
|
||||
def __init__(self, sensor_value, sensor_class, hass, re_arm_sec=60):
|
||||
super(ZWaveTriggerSensor, self).__init__(sensor_value, sensor_class)
|
||||
self._hass = hass
|
||||
self.re_arm_sec = re_arm_sec
|
||||
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 sensor_value.data:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state()
|
||||
if value.data:
|
||||
# only allow this value to be true for re_arm secs
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if movement has happened within the rearm time."""
|
||||
return self._value.data and \
|
||||
(self.invalidate_after is None or
|
||||
self.invalidate_after > dt_util.utcnow())
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
homeassistant.components.bloomsky
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for BloomSky weather station.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/bloomsky/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = "bloomsky"
|
||||
BLOOMSKY = None
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The BloomSky only updates every 5-8 minutes as per the API spec so there's
|
||||
# no point in polling the API more frequently
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||
|
||||
DISCOVER_SENSORS = 'bloomsky.sensors'
|
||||
DISCOVER_BINARY_SENSORS = 'bloomsky.binary_sensor'
|
||||
DISCOVER_CAMERAS = 'bloomsky.camera'
|
||||
|
||||
|
||||
# pylint: disable=unused-argument,too-few-public-methods
|
||||
def setup(hass, config):
|
||||
""" Setup BloomSky component. """
|
||||
if not validate_config(
|
||||
config,
|
||||
{DOMAIN: [CONF_API_KEY]},
|
||||
_LOGGER):
|
||||
return False
|
||||
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
|
||||
global BLOOMSKY
|
||||
try:
|
||||
BLOOMSKY = BloomSky(api_key)
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
for component, discovery_service in (
|
||||
('camera', DISCOVER_CAMERAS), ('sensor', DISCOVER_SENSORS),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS)):
|
||||
discovery.discover(hass, discovery_service, component=component,
|
||||
hass_config=config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BloomSky(object):
|
||||
""" Handle all communication with the BloomSky API. """
|
||||
|
||||
# API documentation at http://weatherlution.com/bloomsky-api/
|
||||
|
||||
API_URL = "https://api.bloomsky.com/api/skydata"
|
||||
|
||||
def __init__(self, api_key):
|
||||
self._api_key = api_key
|
||||
self.devices = {}
|
||||
_LOGGER.debug("Initial bloomsky device load...")
|
||||
self.refresh_devices()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def refresh_devices(self):
|
||||
"""
|
||||
Uses the API to retreive a list of devices associated with an
|
||||
account along with all the sensors on the device.
|
||||
"""
|
||||
_LOGGER.debug("Fetching bloomsky update")
|
||||
response = requests.get(self.API_URL,
|
||||
headers={"Authorization": self._api_key},
|
||||
timeout=10)
|
||||
if response.status_code == 401:
|
||||
raise RuntimeError("Invalid API_KEY")
|
||||
elif response.status_code != 200:
|
||||
_LOGGER.error("Invalid HTTP response: %s", response.status_code)
|
||||
return
|
||||
# create dictionary keyed off of the device unique id
|
||||
self.devices.update({
|
||||
device["DeviceID"]: device for device in response.json()
|
||||
})
|
||||
@@ -1,21 +1,18 @@
|
||||
"""
|
||||
homeassistant.components.browser
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides functionality to launch a webbrowser on the host machine.
|
||||
Provides functionality to launch a web browser on the host machine.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/browser/
|
||||
"""
|
||||
|
||||
DOMAIN = "browser"
|
||||
|
||||
SERVICE_BROWSE_URL = "browse_url"
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Listen for browse_url events and open
|
||||
the url in the default webbrowser. """
|
||||
|
||||
"""
|
||||
Listen for browse_url events and open the url in the default web browser.
|
||||
"""
|
||||
import webbrowser
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_BROWSE_URL,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# pylint: disable=too-many-lines
|
||||
"""
|
||||
homeassistant.components.camera
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with various cameras.
|
||||
Component to interface with cameras.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
@@ -15,8 +13,8 @@ import requests
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components import bloomsky
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE,
|
||||
HTTP_NOT_FOUND,
|
||||
ATTR_ENTITY_ID,
|
||||
)
|
||||
@@ -24,34 +22,19 @@ from homeassistant.const import (
|
||||
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
GROUP_NAME_ALL_CAMERAS = 'all_cameras'
|
||||
SCAN_INTERVAL = 30
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SWITCH_ACTION_RECORD = 'record'
|
||||
SWITCH_ACTION_SNAPSHOT = 'snapshot'
|
||||
|
||||
SERVICE_CAMERA = 'camera_service'
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
bloomsky.DISCOVER_CAMERAS: 'bloomsky',
|
||||
}
|
||||
|
||||
STATE_RECORDING = 'recording'
|
||||
|
||||
DEFAULT_RECORDING_SECONDS = 30
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {}
|
||||
|
||||
FILE_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S-%f'
|
||||
DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
|
||||
|
||||
REC_DIR_PREFIX = 'recording-'
|
||||
REC_IMG_PREFIX = 'recording_image-'
|
||||
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
|
||||
CAMERA_PROXY_URL = '/api/camera_proxy_stream/{0}'
|
||||
CAMERA_STILL_URL = '/api/camera_proxy/{0}'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?time={1}'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
|
||||
|
||||
MULTIPART_BOUNDARY = '--jpegboundary'
|
||||
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
|
||||
@@ -59,8 +42,7 @@ MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for cameras. """
|
||||
|
||||
"""Initialize camera component."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
@@ -79,7 +61,7 @@ def setup(hass, config):
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _proxy_camera_image(handler, path_match, data):
|
||||
""" Proxies the camera image via the HA server. """
|
||||
"""Serve the camera image via the HA server."""
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
camera = component.entities.get(entity_id)
|
||||
|
||||
@@ -105,7 +87,8 @@ def setup(hass, config):
|
||||
# pylint: disable=unused-argument
|
||||
def _proxy_camera_mjpeg_stream(handler, path_match, data):
|
||||
"""
|
||||
Proxies the camera image as an mjpeg stream via the HA server.
|
||||
Proxy the camera image as an mjpeg stream via the HA server.
|
||||
|
||||
This function takes still images from the IP camera and turns them
|
||||
into an MJPEG stream. This means that HA can return a live video
|
||||
stream even with only a still image URL available.
|
||||
@@ -121,33 +104,7 @@ def setup(hass, config):
|
||||
try:
|
||||
camera.is_streaming = True
|
||||
camera.update_ha_state()
|
||||
|
||||
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes(
|
||||
'Content-type: multipart/x-mixed-replace; \
|
||||
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
# MJPEG_START_HEADER.format()
|
||||
|
||||
while True:
|
||||
img_bytes = camera.camera_image()
|
||||
if img_bytes is None:
|
||||
continue
|
||||
headers_str = '\r\n'.join((
|
||||
'Content-length: {}'.format(len(img_bytes)),
|
||||
'Content-type: image/jpeg',
|
||||
)) + '\r\n\r\n'
|
||||
|
||||
handler.request.sendall(
|
||||
bytes(headers_str, 'utf-8') +
|
||||
img_bytes +
|
||||
bytes('\r\n', 'utf-8'))
|
||||
|
||||
handler.request.sendall(
|
||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
time.sleep(0.5)
|
||||
camera.mjpeg_stream(handler)
|
||||
|
||||
except (requests.RequestException, IOError):
|
||||
camera.is_streaming = False
|
||||
@@ -163,36 +120,75 @@ def setup(hass, config):
|
||||
|
||||
|
||||
class Camera(Entity):
|
||||
""" The base class for camera components. """
|
||||
"""The base class for camera entities."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a camera."""
|
||||
self.is_streaming = False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No need to poll cameras."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Return a link to the camera feed as entity picture."""
|
||||
return ENTITY_IMAGE_URL.format(self.entity_id)
|
||||
|
||||
@property
|
||||
# pylint: disable=no-self-use
|
||||
def is_recording(self):
|
||||
""" Returns true if the device is recording. """
|
||||
"""Return true if the device is recording."""
|
||||
return False
|
||||
|
||||
@property
|
||||
# pylint: disable=no-self-use
|
||||
def brand(self):
|
||||
""" Should return a string of the camera brand. """
|
||||
"""Camera brand."""
|
||||
return None
|
||||
|
||||
@property
|
||||
# pylint: disable=no-self-use
|
||||
def model(self):
|
||||
""" Returns string of camera model. """
|
||||
"""Camera model."""
|
||||
return None
|
||||
|
||||
def camera_image(self):
|
||||
""" Return bytes of camera image. """
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes(
|
||||
'Content-type: multipart/x-mixed-replace; \
|
||||
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
||||
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
# MJPEG_START_HEADER.format()
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
if img_bytes is None:
|
||||
continue
|
||||
headers_str = '\r\n'.join((
|
||||
'Content-length: {}'.format(len(img_bytes)),
|
||||
'Content-type: image/jpeg',
|
||||
)) + '\r\n\r\n'
|
||||
|
||||
handler.request.sendall(
|
||||
bytes(headers_str, 'utf-8') +
|
||||
img_bytes +
|
||||
bytes('\r\n', 'utf-8'))
|
||||
|
||||
handler.request.sendall(
|
||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the entity. """
|
||||
"""Camera state."""
|
||||
if self.is_recording:
|
||||
return STATE_RECORDING
|
||||
elif self.is_streaming:
|
||||
@@ -202,11 +198,8 @@ class Camera(Entity):
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
attr = {
|
||||
ATTR_ENTITY_PICTURE: ENTITY_IMAGE_URL.format(
|
||||
self.entity_id, time.time()),
|
||||
}
|
||||
"""Camera state attributes."""
|
||||
attr = {}
|
||||
|
||||
if self.model:
|
||||
attr['model_name'] = self.model
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
homeassistant.components.camera.bloomsky
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for a camera of a BloomSky weather station.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.bloomsky/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ["bloomsky"]
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" set up access to BloomSky cameras """
|
||||
bloomsky = get_component('bloomsky')
|
||||
for device in bloomsky.BLOOMSKY.devices.values():
|
||||
add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
|
||||
|
||||
|
||||
class BloomSkyCamera(Camera):
|
||||
""" Represents the images published from the BloomSky's camera. """
|
||||
|
||||
def __init__(self, bs, device):
|
||||
""" set up for access to the BloomSky camera images """
|
||||
super(BloomSkyCamera, self).__init__()
|
||||
self._name = device["DeviceName"]
|
||||
self._id = device["DeviceID"]
|
||||
self._bloomsky = bs
|
||||
self._url = ""
|
||||
self._last_url = ""
|
||||
# _last_image will store images as they are downloaded so that the
|
||||
# frequent updates in home-assistant don't keep poking the server
|
||||
# to download the same image over and over
|
||||
self._last_image = ""
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
def camera_image(self):
|
||||
""" Update the camera's image if it has changed. """
|
||||
try:
|
||||
self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"]
|
||||
self._bloomsky.refresh_devices()
|
||||
# if the url hasn't changed then the image hasn't changed
|
||||
if self._url != self._last_url:
|
||||
response = requests.get(self._url, timeout=10)
|
||||
self._last_url = self._url
|
||||
self._last_image = response.content
|
||||
except requests.exceptions.RequestException as error:
|
||||
self._logger.error("Error getting bloomsky image: %s", error)
|
||||
return None
|
||||
|
||||
return self._last_image
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of this BloomSky device. """
|
||||
return self._name
|
||||
@@ -1,29 +1,31 @@
|
||||
"""
|
||||
homeassistant.components.camera.demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Demo platform that has a fake camera.
|
||||
Demo camera platform that has a fake camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import os
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Demo camera. """
|
||||
"""Setup the Demo camera platform."""
|
||||
add_devices([
|
||||
DemoCamera('Demo camera')
|
||||
])
|
||||
|
||||
|
||||
class DemoCamera(Camera):
|
||||
""" A Demo camera. """
|
||||
"""A Demo camera."""
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__()
|
||||
self._name = name
|
||||
|
||||
def camera_image(self):
|
||||
""" Return a faked still image response. """
|
||||
"""Return a faked still image response."""
|
||||
now = dt_util.utcnow()
|
||||
|
||||
image_path = os.path.join(os.path.dirname(__file__),
|
||||
@@ -33,5 +35,5 @@ class DemoCamera(Camera):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Return the name of this device. """
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@@ -10,8 +10,8 @@ import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import logging
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -6,14 +6,17 @@ Support for IP Cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.mjpeg/
|
||||
"""
|
||||
from contextlib import closing
|
||||
import logging
|
||||
from contextlib import closing
|
||||
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.const import HTTP_OK
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,6 +44,17 @@ class MjpegCamera(Camera):
|
||||
self._password = device_info.get('password')
|
||||
self._mjpeg_url = device_info['mjpeg_url']
|
||||
|
||||
def camera_stream(self):
|
||||
""" Return a mjpeg stream image response directly from the camera. """
|
||||
if self._username and self._password:
|
||||
return requests.get(self._mjpeg_url,
|
||||
auth=HTTPBasicAuth(self._username,
|
||||
self._password),
|
||||
stream=True)
|
||||
else:
|
||||
return requests.get(self._mjpeg_url,
|
||||
stream=True)
|
||||
|
||||
def camera_image(self):
|
||||
""" Return a still image response from the camera. """
|
||||
|
||||
@@ -55,16 +69,22 @@ class MjpegCamera(Camera):
|
||||
jpg = data[jpg_start:jpg_end + 2]
|
||||
return jpg
|
||||
|
||||
if self._username and self._password:
|
||||
with closing(requests.get(self._mjpeg_url,
|
||||
auth=HTTPBasicAuth(self._username,
|
||||
self._password),
|
||||
stream=True)) as response:
|
||||
return process_response(response)
|
||||
else:
|
||||
with closing(requests.get(self._mjpeg_url,
|
||||
stream=True)) as response:
|
||||
return process_response(response)
|
||||
with closing(self.camera_stream()) as response:
|
||||
return process_response(response)
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
""" Generate an HTTP MJPEG stream from the camera. """
|
||||
response = self.camera_stream()
|
||||
content_type = response.headers[CONTENT_TYPE_HEADER]
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header(CONTENT_TYPE_HEADER, content_type)
|
||||
handler.end_headers()
|
||||
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if not chunk:
|
||||
break
|
||||
handler.wfile.write(chunk)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
homeassistant.components.camera.uvc
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Ubiquiti's UVC cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.uvc/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Discover cameras on a Unifi NVR. """
|
||||
if not validate_config({DOMAIN: config}, {DOMAIN: ['nvr', 'key']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
addr = config.get('nvr')
|
||||
key = config.get('key')
|
||||
try:
|
||||
port = int(config.get('port', 7080))
|
||||
except ValueError:
|
||||
_LOGGER.error('Invalid port number provided')
|
||||
return False
|
||||
|
||||
from uvcclient import nvr
|
||||
nvrconn = nvr.UVCRemote(addr, port, key)
|
||||
try:
|
||||
cameras = nvrconn.index()
|
||||
except nvr.NotAuthorized:
|
||||
_LOGGER.error('Authorization failure while connecting to NVR')
|
||||
return False
|
||||
except nvr.NvrError:
|
||||
_LOGGER.error('NVR refuses to talk to me')
|
||||
return False
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to NVR: %s', str(ex))
|
||||
return False
|
||||
|
||||
# Filter out airCam models, which are not supported in the latest
|
||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||
cameras = [camera for camera in cameras
|
||||
if 'airCam' not in nvrconn.get_camera(camera['uuid'])['model']]
|
||||
|
||||
add_devices([UnifiVideoCamera(nvrconn,
|
||||
camera['uuid'],
|
||||
camera['name'])
|
||||
for camera in cameras])
|
||||
return True
|
||||
|
||||
|
||||
class UnifiVideoCamera(Camera):
|
||||
""" A Ubiquiti Unifi Video Camera. """
|
||||
|
||||
def __init__(self, nvr, uuid, name):
|
||||
super(UnifiVideoCamera, self).__init__()
|
||||
self._nvr = nvr
|
||||
self._uuid = uuid
|
||||
self._name = name
|
||||
self.is_streaming = False
|
||||
self._connect_addr = None
|
||||
self._camera = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
return caminfo['recordingSettings']['fullTimeRecordEnabled']
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
return 'Ubiquiti'
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
return caminfo['model']
|
||||
|
||||
def _login(self):
|
||||
from uvcclient import camera as uvc_camera
|
||||
from uvcclient import store as uvc_store
|
||||
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
if self._connect_addr:
|
||||
addrs = [self._connect_addr]
|
||||
else:
|
||||
addrs = [caminfo['host'], caminfo['internalHost']]
|
||||
|
||||
store = uvc_store.get_info_store()
|
||||
password = store.get_camera_password(self._uuid)
|
||||
if password is None:
|
||||
_LOGGER.debug('Logging into camera %(name)s with default password',
|
||||
dict(name=self._name))
|
||||
password = 'ubnt'
|
||||
|
||||
camera = None
|
||||
for addr in addrs:
|
||||
try:
|
||||
camera = uvc_camera.UVCCameraClient(addr,
|
||||
caminfo['username'],
|
||||
password)
|
||||
camera.login()
|
||||
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
|
||||
dict(name=self._name, addr=addr))
|
||||
self._connect_addr = addr
|
||||
break
|
||||
except socket.error:
|
||||
pass
|
||||
except uvc_camera.CameraConnectError:
|
||||
pass
|
||||
except uvc_camera.CameraAuthError:
|
||||
pass
|
||||
if not self._connect_addr:
|
||||
_LOGGER.error('Unable to login to camera')
|
||||
return None
|
||||
|
||||
self._camera = camera
|
||||
return True
|
||||
|
||||
def camera_image(self):
|
||||
from uvcclient import camera as uvc_camera
|
||||
if not self._camera:
|
||||
if not self._login():
|
||||
return
|
||||
|
||||
def _get_image(retry=True):
|
||||
try:
|
||||
return self._camera.get_snapshot()
|
||||
except uvc_camera.CameraConnectError:
|
||||
_LOGGER.error('Unable to contact camera')
|
||||
except uvc_camera.CameraAuthError:
|
||||
if retry:
|
||||
self._login()
|
||||
return _get_image(retry=False)
|
||||
else:
|
||||
_LOGGER.error('Unable to log into camera, unable '
|
||||
'to get snapshot')
|
||||
raise
|
||||
|
||||
return _get_image()
|
||||
@@ -11,8 +11,8 @@ the user has submitted configuration information.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.const import EVENT_TIME_CHANGED
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
DOMAIN = "configurator"
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
@@ -141,7 +141,7 @@ class Configurator(object):
|
||||
|
||||
state = self.hass.states.get(entity_id)
|
||||
|
||||
new_data = state.attributes
|
||||
new_data = dict(state.attributes)
|
||||
new_data[ATTR_ERRORS] = error
|
||||
|
||||
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
|
||||
|
||||
@@ -9,10 +9,9 @@ https://home-assistant.io/components/conversation/
|
||||
import logging
|
||||
import re
|
||||
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
|
||||
DOMAIN = "conversation"
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
"""
|
||||
homeassistant.components.demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Sets up a demo environment that mimics interaction with devices.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import time
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.const import (
|
||||
CONF_PLATFORM, ATTR_ENTITY_ID)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
|
||||
DOMAIN = "demo"
|
||||
|
||||
@@ -21,6 +20,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'binary_sensor',
|
||||
'camera',
|
||||
'device_tracker',
|
||||
'garage_door',
|
||||
'light',
|
||||
'lock',
|
||||
'media_player',
|
||||
@@ -33,7 +33,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup a demo environment. """
|
||||
"""Setup a demo environment."""
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
|
||||
@@ -63,14 +63,29 @@ def setup(hass, config):
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
media_players = sorted(hass.states.entity_ids('media_player'))
|
||||
group.Group(hass, 'living room', [
|
||||
lights[2], lights[1], switches[0], media_players[1],
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'scene.romantic_lights'])
|
||||
group.Group(hass, 'bedroom', [lights[0], switches[1],
|
||||
media_players[0]])
|
||||
group.Group(hass, 'Rooms', [
|
||||
'group.living_room', 'group.bedroom',
|
||||
group.Group(hass, 'bedroom', [lights[0], switches[1], media_players[0]])
|
||||
group.Group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
|
||||
group.Group(hass, 'doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door'])
|
||||
group.Group(hass, 'automations', [
|
||||
'input_select.who_cooks', 'input_boolean.notify', ])
|
||||
group.Group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus'])
|
||||
group.Group(hass, 'thermostats', [
|
||||
'thermostat.nest', 'thermostat.thermostat'])
|
||||
group.Group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window',
|
||||
'rollershutter.living_room_window', 'group.doors', 'thermostat.nest',
|
||||
], view=True)
|
||||
group.Group(hass, 'Upstairs', [
|
||||
'thermostat.thermostat', 'group.bedroom',
|
||||
], view=True)
|
||||
|
||||
# Setup scripts
|
||||
@@ -112,11 +127,28 @@ def setup(hass, config):
|
||||
}},
|
||||
]})
|
||||
|
||||
# Set up input select
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_select',
|
||||
{'input_select':
|
||||
{'living_room_preset': {'options': ['Visitors',
|
||||
'Visitors with kids',
|
||||
'Home Alone']},
|
||||
'who_cooks': {'icon': 'mdi:panda',
|
||||
'initial': 'Anne Therese',
|
||||
'name': 'Who cooks today',
|
||||
'options': ['Paulus', 'Anne Therese']}}})
|
||||
# Set up input boolean
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_boolean',
|
||||
{'input_boolean': {'notify': {'icon': 'mdi:car',
|
||||
'initial': False,
|
||||
'name': 'Notify Anne Therese is home'}}})
|
||||
# Setup configurator
|
||||
configurator_ids = []
|
||||
|
||||
def hue_configuration_callback(data):
|
||||
""" Fake callback, mark config as done. """
|
||||
"""Fake callback, mark config as done."""
|
||||
time.sleep(2)
|
||||
|
||||
# First time it is called, pretend it failed.
|
||||
|
||||
@@ -10,10 +10,11 @@ https://home-assistant.io/components/device_sun_light_trigger/
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.helpers.event import track_point_in_time, track_state_change
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||
from . import light, sun, device_tracker, group
|
||||
from homeassistant.helpers.event import track_point_in_time, track_state_change
|
||||
|
||||
from . import device_tracker, group, light, sun
|
||||
|
||||
DOMAIN = "device_sun_light_trigger"
|
||||
DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun']
|
||||
|
||||
@@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
DOMAIN = "device_tracker"
|
||||
@@ -297,14 +297,16 @@ class Device(Entity):
|
||||
""" State of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Picture of the device."""
|
||||
return self.config_picture
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Device state attributes. """
|
||||
attr = {}
|
||||
|
||||
if self.config_picture:
|
||||
attr[ATTR_ENTITY_PICTURE] = self.config_picture
|
||||
|
||||
if self.gps:
|
||||
attr[ATTR_LATITUDE] = self.gps[0]
|
||||
attr[ATTR_LONGITUDE] = self.gps[1]
|
||||
|
||||
@@ -8,17 +8,17 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.actiontec/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from collections import namedtuple
|
||||
import re
|
||||
import threading
|
||||
import telnetlib
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -8,19 +8,19 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.aruba/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import threading
|
||||
import telnetlib
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DEVICES_REGEX = re.compile(
|
||||
@@ -44,6 +44,7 @@ def get_scanner(hass, config):
|
||||
|
||||
class ArubaDeviceScanner(object):
|
||||
""" This class queries a Aruba Acces Point for connected devices. """
|
||||
|
||||
def __init__(self, config):
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
@@ -93,23 +94,39 @@ class ArubaDeviceScanner(object):
|
||||
|
||||
def get_aruba_data(self):
|
||||
""" Retrieve data from Aruba Access Point and return parsed result. """
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'User: ')
|
||||
telnet.write((self.username + '\r\n').encode('ascii'))
|
||||
telnet.read_until(b'Password: ')
|
||||
telnet.write((self.password + '\r\n').encode('ascii'))
|
||||
telnet.read_until(b'#')
|
||||
telnet.write(('show clients\r\n').encode('ascii'))
|
||||
devices_result = telnet.read_until(b'#').split(b'\r\n')
|
||||
telnet.write('exit\r\n'.encode('ascii'))
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
|
||||
import pexpect
|
||||
connect = "ssh {}@{}"
|
||||
ssh = pexpect.spawn(connect.format(self.username, self.host))
|
||||
query = ssh.expect(['password:', pexpect.TIMEOUT, pexpect.EOF,
|
||||
'continue connecting (yes/no)?',
|
||||
'Host key verification failed.',
|
||||
'Connection refused',
|
||||
'Connection timed out'], timeout=120)
|
||||
if query == 1:
|
||||
_LOGGER.error("Timeout")
|
||||
return
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router," +
|
||||
" is telnet enabled?")
|
||||
elif query == 2:
|
||||
_LOGGER.error("Unexpected response from router")
|
||||
return
|
||||
elif query == 3:
|
||||
ssh.sendline('yes')
|
||||
ssh.expect('password:')
|
||||
elif query == 4:
|
||||
_LOGGER.error("Host key Changed")
|
||||
return
|
||||
elif query == 5:
|
||||
_LOGGER.error("Connection refused by server")
|
||||
return
|
||||
elif query == 6:
|
||||
_LOGGER.error("Connection timed out")
|
||||
return
|
||||
ssh.sendline(self.password)
|
||||
ssh.expect('#')
|
||||
ssh.sendline('show clients')
|
||||
ssh.expect('#')
|
||||
devices_result = ssh.before.split(b'\r\n')
|
||||
ssh.sendline('exit')
|
||||
|
||||
devices = {}
|
||||
for device in devices_result:
|
||||
@@ -119,5 +136,5 @@ class ArubaDeviceScanner(object):
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
'name': match.group('name')
|
||||
}
|
||||
}
|
||||
return devices
|
||||
|
||||
@@ -8,15 +8,15 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.asuswrt/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import threading
|
||||
import telnetlib
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -8,15 +8,16 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.ddwrt/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -10,10 +10,10 @@ https://home-assistant.io/components/device_tracker.fritz/
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.4.6']
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.icloud/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import re
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -9,9 +9,8 @@ https://home-assistant.io/components/device_tracker.locative/
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.const import (
|
||||
HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -7,17 +7,18 @@ presence.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.luci/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -7,8 +7,9 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.mqtt/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant import util
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant import util
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.netgear/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -7,16 +7,16 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.nmap_scanner/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from collections import namedtuple
|
||||
import subprocess
|
||||
import re
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOSTS
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle, convert
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -62,7 +62,7 @@ def setup_scanner(hass, config, see):
|
||||
see_beacons(dev_id, kwargs)
|
||||
|
||||
def owntracks_event_update(topic, payload, qos):
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
""" MQTT event (geofences) received. """
|
||||
|
||||
# Docs on available data:
|
||||
@@ -92,10 +92,14 @@ def setup_scanner(hass, config, see):
|
||||
if zone is None:
|
||||
if data['t'] == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
MOBILE_BEACONS_ACTIVE[dev_id].append(location)
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location not in beacons:
|
||||
beacons.append(location)
|
||||
_LOGGER.info("Added beacon %s", location)
|
||||
else:
|
||||
# Normal region
|
||||
kwargs['location_name'] = location
|
||||
if not zone.attributes.get('passive'):
|
||||
kwargs['location_name'] = location
|
||||
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location not in regions:
|
||||
@@ -107,27 +111,30 @@ def setup_scanner(hass, config, see):
|
||||
see_beacons(dev_id, kwargs)
|
||||
|
||||
elif data['event'] == 'leave':
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location in regions:
|
||||
regions.remove(location)
|
||||
new_region = regions[-1] if regions else None
|
||||
with LOCK:
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location in regions:
|
||||
regions.remove(location)
|
||||
new_region = regions[-1] if regions else None
|
||||
|
||||
if new_region:
|
||||
# Exit to previous region
|
||||
zone = hass.states.get("zone.{}".format(new_region))
|
||||
kwargs['location_name'] = new_region
|
||||
_set_gps_from_zone(kwargs, zone)
|
||||
_LOGGER.info("Exit from to %s", new_region)
|
||||
if new_region:
|
||||
# Exit to previous region
|
||||
zone = hass.states.get("zone.{}".format(new_region))
|
||||
if not zone.attributes.get('passive'):
|
||||
kwargs['location_name'] = new_region
|
||||
_set_gps_from_zone(kwargs, zone)
|
||||
_LOGGER.info("Exit to %s", new_region)
|
||||
|
||||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
_LOGGER.info("Remove beacon %s", location)
|
||||
|
||||
else:
|
||||
_LOGGER.error(
|
||||
@@ -139,6 +146,8 @@ def setup_scanner(hass, config, see):
|
||||
""" Set active beacons to the current location """
|
||||
|
||||
kwargs = kwargs_param.copy()
|
||||
# the battery state applies to the tracking device, not the beacon
|
||||
kwargs.pop('battery', None)
|
||||
for beacon in MOBILE_BEACONS_ACTIVE[dev_id]:
|
||||
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
|
||||
kwargs['host_name'] = beacon
|
||||
|
||||
@@ -7,15 +7,15 @@ through SNMP.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.snmp/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import threading
|
||||
import binascii
|
||||
import logging
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
@@ -8,15 +8,15 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.thomson/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import threading
|
||||
import telnetlib
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
@@ -7,18 +7,18 @@ presence.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.tomato/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -9,15 +9,16 @@ https://home-assistant.io/components/device_tracker.tplink/
|
||||
"""
|
||||
import base64
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -7,17 +7,18 @@ presence.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.ubus/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.unifi
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a Unifi WAP controller
|
||||
"""
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
# Unifi package doesn't list urllib3 as a requirement
|
||||
REQUIREMENTS = ['urllib3', 'unifi==1.2.4']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
""" Sets up unifi device_tracker """
|
||||
from unifi.controller import Controller
|
||||
|
||||
if not validate_config(config, {DOMAIN: [CONF_USERNAME,
|
||||
CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
_LOGGER.error('Invalid configuration')
|
||||
return False
|
||||
|
||||
this_config = config[DOMAIN]
|
||||
host = this_config.get(CONF_HOST, 'localhost')
|
||||
username = this_config.get(CONF_USERNAME)
|
||||
password = this_config.get(CONF_PASSWORD)
|
||||
|
||||
try:
|
||||
port = int(this_config.get(CONF_PORT, 8443))
|
||||
except ValueError:
|
||||
_LOGGER.error('Invalid port (must be numeric like 8443)')
|
||||
return False
|
||||
|
||||
try:
|
||||
ctrl = Controller(host, username, password, port, 'v4')
|
||||
except urllib.error.HTTPError as ex:
|
||||
_LOGGER.error('Failed to connect to unifi: %s', ex)
|
||||
return False
|
||||
|
||||
return UnifiScanner(ctrl)
|
||||
|
||||
|
||||
class UnifiScanner(object):
|
||||
"""Provide device_tracker support from Unifi WAP client data."""
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
try:
|
||||
clients = self._controller.get_clients()
|
||||
except urllib.error.HTTPError as ex:
|
||||
_LOGGER.error('Failed to scan clients: %s', ex)
|
||||
clients = []
|
||||
|
||||
self._clients = {client['mac']: client for client in clients}
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for devices. """
|
||||
self._update()
|
||||
return self._clients.keys()
|
||||
|
||||
def get_device_name(self, mac):
|
||||
""" Returns the name (if known) of the device.
|
||||
|
||||
If a name has been set in Unifi, then return that, else
|
||||
return the hostname if it has been detected.
|
||||
"""
|
||||
client = self._clients.get(mac, {})
|
||||
name = client.get('name') or client.get('hostname')
|
||||
_LOGGER.debug('Device %s name %s', mac, name)
|
||||
return name
|
||||
@@ -1,6 +1,4 @@
|
||||
"""
|
||||
homeassistant.components.discovery
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Starts a service to scan in intervals for new devices.
|
||||
|
||||
Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
|
||||
@@ -13,8 +11,8 @@ import threading
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_PLATFORM_DISCOVERED,
|
||||
ATTR_SERVICE, ATTR_DISCOVERED)
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_HOMEASSISTANT_START,
|
||||
EVENT_PLATFORM_DISCOVERED)
|
||||
|
||||
DOMAIN = "discovery"
|
||||
REQUIREMENTS = ['netdisco==0.5.2']
|
||||
@@ -29,7 +27,7 @@ SERVICE_SONOS = 'sonos'
|
||||
SERVICE_PLEX = 'plex_mediaserver'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_WEMO: "switch",
|
||||
SERVICE_WEMO: "wemo",
|
||||
SERVICE_CAST: "media_player",
|
||||
SERVICE_HUE: "light",
|
||||
SERVICE_NETGEAR: 'device_tracker',
|
||||
@@ -39,24 +37,41 @@ SERVICE_HANDLERS = {
|
||||
|
||||
|
||||
def listen(hass, service, callback):
|
||||
"""
|
||||
Setup listener for discovery of specific service.
|
||||
"""Setup listener for discovery of specific service.
|
||||
|
||||
Service can be a string or a list/tuple.
|
||||
"""
|
||||
|
||||
if isinstance(service, str):
|
||||
service = (service,)
|
||||
else:
|
||||
service = tuple(service)
|
||||
|
||||
def discovery_event_listener(event):
|
||||
""" Listens for discovery events. """
|
||||
"""Listen for discovery events."""
|
||||
if event.data[ATTR_SERVICE] in service:
|
||||
callback(event.data[ATTR_SERVICE], event.data[ATTR_DISCOVERED])
|
||||
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
|
||||
|
||||
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
|
||||
|
||||
|
||||
def discover(hass, service, discovered=None, component=None, hass_config=None):
|
||||
"""Fire discovery event.
|
||||
|
||||
Can ensure a component is loaded.
|
||||
"""
|
||||
if component is not None:
|
||||
bootstrap.setup_component(hass, component, hass_config)
|
||||
|
||||
data = {
|
||||
ATTR_SERVICE: service
|
||||
}
|
||||
|
||||
if discovered is not None:
|
||||
data[ATTR_DISCOVERED] = discovered
|
||||
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Starts a discovery service. """
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -6,8 +6,8 @@ Provides functionality to download files.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/downloader/
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
|
||||
|
||||
@@ -1,40 +1,20 @@
|
||||
"""
|
||||
homeassistant.components.ecobee
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Ecobee Component
|
||||
|
||||
This component adds support for Ecobee3 Wireless Thermostats.
|
||||
You will need to setup developer access to your thermostat,
|
||||
and create and API key on the ecobee website.
|
||||
|
||||
The first time you run this component you will see a configuration
|
||||
component card in Home Assistant. This card will contain a PIN code
|
||||
that you will need to use to authorize access to your thermostat. You
|
||||
can do this at https://www.ecobee.com/consumerportal/index.html
|
||||
Click My Apps, Add application, Enter Pin and click Authorize.
|
||||
|
||||
After authorizing the application click the button in the configuration
|
||||
card. Now your thermostat and sensors should shown in home-assistant.
|
||||
|
||||
You can use the optional hold_temp parameter to set whether or not holds
|
||||
are set indefintely or until the next scheduled event.
|
||||
|
||||
ecobee:
|
||||
api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf
|
||||
hold_temp: True
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Ecobee component
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ecobee/
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.const import (
|
||||
EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_API_KEY)
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = "ecobee"
|
||||
DISCOVER_THERMOSTAT = "ecobee.thermostat"
|
||||
@@ -82,7 +62,7 @@ def request_configuration(network, hass, config):
|
||||
|
||||
|
||||
def setup_ecobee(hass, network, config):
|
||||
""" Setup ecobee thermostat """
|
||||
""" Setup Ecobee thermostat. """
|
||||
# If ecobee has a PIN then it needs to be configured.
|
||||
if network.pin is not None:
|
||||
request_configuration(network, hass, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
from . import version, mdi_version
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import URL_ROOT, HTTP_OK
|
||||
from homeassistant.components import api
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
@@ -25,21 +26,23 @@ FRONTEND_URLS = [
|
||||
re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)'),
|
||||
]
|
||||
|
||||
URL_API_BOOTSTRAP = "/api/bootstrap"
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup serving the frontend. """
|
||||
if 'http' not in hass.config.components:
|
||||
_LOGGER.error('Dependency http is not loaded')
|
||||
return False
|
||||
|
||||
for url in FRONTEND_URLS:
|
||||
hass.http.register_path('GET', url, _handle_get_root, False)
|
||||
|
||||
hass.http.register_path('GET', '/service_worker.js',
|
||||
_handle_get_service_worker, False)
|
||||
|
||||
# Bootstrap API
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
|
||||
|
||||
# Static files
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
@@ -54,6 +57,18 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_bootstrap(handler, path_match, data):
|
||||
""" Returns all data needed to bootstrap Home Assistant. """
|
||||
hass = handler.server.hass
|
||||
|
||||
handler.write_json({
|
||||
'config': hass.config.as_dict(),
|
||||
'states': hass.states.all(),
|
||||
'events': api.events_json(hass),
|
||||
'services': api.services_json(hass),
|
||||
})
|
||||
|
||||
|
||||
def _handle_get_root(handler, path_match, data):
|
||||
""" Renders the frontend. """
|
||||
handler.send_response(HTTP_OK)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by update_mdi script """
|
||||
VERSION = "a2605736c8d959d50c4bcbba1e6a6aa5"
|
||||
VERSION = "2f4adc5d3ad6d2f73bf69ed29b7594fd"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "1e89871aaae43c91b2508f52bc161b69"
|
||||
VERSION = "a4d021cb50ed079fcfda7369ed2f0d4a"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
|
||||
!*** ./src/service-worker/index.js ***!
|
||||
\*************************************/
|
||||
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){var r=fetch(e.request).then(function(e){return t.put(s,e.clone()),e});return n||r})}))})}]);
|
||||
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}]);
|
||||
//# sourceMappingURL=service_worker.js.map
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Component to interface with garage doors that can be controlled remotely.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
at https://home-assistant.io/components/garage_door/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.components import (group, wink)
|
||||
|
||||
DOMAIN = 'garage_door'
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
GROUP_NAME_ALL_GARAGE_DOORS = 'all garage doors'
|
||||
ENTITY_ID_ALL_GARAGE_DOORS = group.ENTITY_ID_FORMAT.format('all_garage_doors')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_GARAGE_DOORS: 'wink'
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_closed(hass, entity_id=None):
|
||||
"""Returns if the garage door is closed based on the statemachine."""
|
||||
entity_id = entity_id or ENTITY_ID_ALL_GARAGE_DOORS
|
||||
return hass.states.is_state(entity_id, STATE_CLOSED)
|
||||
|
||||
|
||||
def close_door(hass, entity_id=None):
|
||||
"""Closes all or specified garage door."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_CLOSE, data)
|
||||
|
||||
|
||||
def open_door(hass, entity_id=None):
|
||||
"""Open all or specified garage door."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_OPEN, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for garage door."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
|
||||
GROUP_NAME_ALL_GARAGE_DOORS)
|
||||
component.setup(config)
|
||||
|
||||
def handle_garage_door_service(service):
|
||||
"""Handles calls to the garage door services."""
|
||||
target_locks = component.extract_from_service(service)
|
||||
|
||||
for item in target_locks:
|
||||
if service.service == SERVICE_CLOSE:
|
||||
item.close_door()
|
||||
else:
|
||||
item.open_door()
|
||||
|
||||
if item.should_poll:
|
||||
item.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_OPEN, handle_garage_door_service,
|
||||
descriptions.get(SERVICE_OPEN))
|
||||
hass.services.register(DOMAIN, SERVICE_CLOSE, handle_garage_door_service,
|
||||
descriptions.get(SERVICE_CLOSE))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GarageDoorDevice(Entity):
|
||||
"""Represents a garage door."""
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if door is closed."""
|
||||
return None
|
||||
|
||||
def close_door(self):
|
||||
"""Close the garage door."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def open_door(self):
|
||||
"""Open the garage door."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Returns the state of the garage door."""
|
||||
closed = self.is_closed
|
||||
if closed is None:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_CLOSED if closed else STATE_OPEN
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Demo garage door platform that has two fake doors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
from homeassistant.const import STATE_CLOSED, STATE_OPEN
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup demo garage door platform."""
|
||||
add_devices_callback([
|
||||
DemoGarageDoor('Left Garage Door', STATE_CLOSED),
|
||||
DemoGarageDoor('Right Garage Door', STATE_OPEN)
|
||||
])
|
||||
|
||||
|
||||
class DemoGarageDoor(GarageDoorDevice):
|
||||
"""Provides a demo garage door."""
|
||||
def __init__(self, name, state):
|
||||
self._name = name
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for a demo garage door."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if garage door is closed."""
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
def close_door(self, **kwargs):
|
||||
"""Close the garage door."""
|
||||
self._state = STATE_CLOSED
|
||||
self.update_ha_state()
|
||||
|
||||
def open_door(self, **kwargs):
|
||||
"""Open the garage door."""
|
||||
self._state = STATE_OPEN
|
||||
self.update_ha_state()
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Support for Wink garage doors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/garage_door.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.6.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Sets up the Wink garage door platform."""
|
||||
import pywink
|
||||
|
||||
if discovery_info is None:
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if token is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing wink access_token. "
|
||||
"Get one at https://winkbearertoken.appspot.com/")
|
||||
return
|
||||
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
add_devices(WinkGarageDoorDevice(door) for door in
|
||||
pywink.get_garage_doors())
|
||||
|
||||
|
||||
class WinkGarageDoorDevice(GarageDoorDevice):
|
||||
"""Represents a Wink garage door."""
|
||||
|
||||
def __init__(self, wink):
|
||||
self.wink = wink
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Returns the id of this wink garage door."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Returns the name of the garage door if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the garage door."""
|
||||
self.wink.update_state()
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Returns true if door is closed."""
|
||||
return self.wink.state() == 0
|
||||
|
||||
def close_door(self):
|
||||
"""Closes the door."""
|
||||
self.wink.set_state(0)
|
||||
|
||||
def open_door(self):
|
||||
"""Open the door."""
|
||||
self.wink.set_state(1)
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Component that records all events and state changes and feeds the data to
|
||||
a Graphite installation.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/graphite/
|
||||
"""
|
||||
import logging
|
||||
import queue
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED)
|
||||
from homeassistant.helpers import state
|
||||
|
||||
DOMAIN = "graphite"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Graphite feeder."""
|
||||
graphite_config = config.get('graphite', {})
|
||||
host = graphite_config.get('host', 'localhost')
|
||||
prefix = graphite_config.get('prefix', 'ha')
|
||||
try:
|
||||
port = int(graphite_config.get('port', 2003))
|
||||
except ValueError:
|
||||
_LOGGER.error('Invalid port specified')
|
||||
return False
|
||||
|
||||
GraphiteFeeder(hass, host, port, prefix)
|
||||
return True
|
||||
|
||||
|
||||
class GraphiteFeeder(threading.Thread):
|
||||
"""Feeds data to Graphite."""
|
||||
def __init__(self, hass, host, port, prefix):
|
||||
super(GraphiteFeeder, self).__init__(daemon=True)
|
||||
self._hass = hass
|
||||
self._host = host
|
||||
self._port = port
|
||||
# rstrip any trailing dots in case they think they need it
|
||||
self._prefix = prefix.rstrip('.')
|
||||
self._queue = queue.Queue()
|
||||
self._quit_object = object()
|
||||
self._we_started = False
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||
self.start_listen)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
self.shutdown)
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, self.event_listener)
|
||||
_LOGGER.debug('Graphite feeding to %s:%i initialized',
|
||||
self._host, self._port)
|
||||
|
||||
def start_listen(self, event):
|
||||
"""Start event-processing thread."""
|
||||
_LOGGER.debug('Event processing thread started')
|
||||
self._we_started = True
|
||||
self.start()
|
||||
|
||||
def shutdown(self, event):
|
||||
"""Signal shutdown of processing event."""
|
||||
_LOGGER.debug('Event processing signaled exit')
|
||||
self._queue.put(self._quit_object)
|
||||
|
||||
def event_listener(self, event):
|
||||
"""Queue an event for processing."""
|
||||
if self.is_alive() or not self._we_started:
|
||||
_LOGGER.debug('Received event')
|
||||
self._queue.put(event)
|
||||
else:
|
||||
_LOGGER.error('Graphite feeder thread has died, not '
|
||||
'queuing event!')
|
||||
|
||||
def _send_to_graphite(self, data):
|
||||
"""Send data to Graphite."""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
sock.connect((self._host, self._port))
|
||||
sock.sendall(data.encode('ascii'))
|
||||
sock.send('\n'.encode('ascii'))
|
||||
sock.close()
|
||||
|
||||
def _report_attributes(self, entity_id, new_state):
|
||||
"""Report the attributes."""
|
||||
now = time.time()
|
||||
things = dict(new_state.attributes)
|
||||
try:
|
||||
things['state'] = state.state_as_number(new_state)
|
||||
except ValueError:
|
||||
pass
|
||||
lines = ['%s.%s.%s %f %i' % (self._prefix,
|
||||
entity_id, key.replace(' ', '_'),
|
||||
value, now)
|
||||
for key, value in things.items()
|
||||
if isinstance(value, (float, int))]
|
||||
if not lines:
|
||||
return
|
||||
_LOGGER.debug('Sending to graphite: %s', lines)
|
||||
try:
|
||||
self._send_to_graphite('\n'.join(lines))
|
||||
except socket.gaierror:
|
||||
_LOGGER.error('Unable to connect to host %s', self._host)
|
||||
except socket.error:
|
||||
_LOGGER.exception('Failed to send data to graphite')
|
||||
|
||||
def run(self):
|
||||
"""Run the process to export the data."""
|
||||
while True:
|
||||
event = self._queue.get()
|
||||
if event == self._quit_object:
|
||||
_LOGGER.debug('Event processing thread stopped')
|
||||
self._queue.task_done()
|
||||
return
|
||||
elif (event.event_type == EVENT_STATE_CHANGED and
|
||||
event.data.get('new_state')):
|
||||
_LOGGER.debug('Processing STATE_CHANGED event for %s',
|
||||
event.data['entity_id'])
|
||||
try:
|
||||
self._report_attributes(event.data['entity_id'],
|
||||
event.data['new_state'])
|
||||
# pylint: disable=broad-except
|
||||
except Exception:
|
||||
# Catch this so we can avoid the thread dying and
|
||||
# make it visible.
|
||||
_LOGGER.exception('Failed to process STATE_CHANGED event')
|
||||
else:
|
||||
_LOGGER.warning('Processing unexpected event type %s',
|
||||
event.event_type)
|
||||
|
||||
self._queue.task_done()
|
||||
@@ -7,13 +7,13 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/group/
|
||||
"""
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.entity import (
|
||||
Entity, split_entity_id, generate_entity_id)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
|
||||
STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED,
|
||||
STATE_UNKNOWN, CONF_NAME, CONF_ICON)
|
||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_UNKNOWN,
|
||||
ATTR_ASSUMED_STATE, )
|
||||
from homeassistant.helpers.entity import (
|
||||
Entity, generate_entity_id, split_entity_id)
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
|
||||
DOMAIN = 'group'
|
||||
|
||||
@@ -71,7 +71,7 @@ def expand_entity_ids(hass, entity_ids):
|
||||
if domain == DOMAIN:
|
||||
found_ids.extend(
|
||||
ent_id for ent_id
|
||||
in get_entity_ids(hass, entity_id)
|
||||
in expand_entity_ids(hass, get_entity_ids(hass, entity_id))
|
||||
if ent_id not in found_ids)
|
||||
|
||||
else:
|
||||
@@ -145,6 +145,7 @@ class Group(Entity):
|
||||
self.tracking = []
|
||||
self.group_on = None
|
||||
self.group_off = None
|
||||
self._assumed_state = False
|
||||
|
||||
if entity_ids is not None:
|
||||
self.update_tracked_entity_ids(entity_ids)
|
||||
@@ -183,6 +184,11 @@ class Group(Entity):
|
||||
data[ATTR_VIEW] = True
|
||||
return data
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return True if unable to access real state of entity."""
|
||||
return self._assumed_state
|
||||
|
||||
def update_tracked_entity_ids(self, entity_ids):
|
||||
""" Update the tracked entity IDs. """
|
||||
self.stop()
|
||||
@@ -208,47 +214,77 @@ class Group(Entity):
|
||||
def update(self):
|
||||
""" Query all the tracked states and determine current group state. """
|
||||
self._state = STATE_UNKNOWN
|
||||
self._update_group_state()
|
||||
|
||||
def _state_changed_listener(self, entity_id, old_state, new_state):
|
||||
""" Listener to receive state changes of tracked entities. """
|
||||
self._update_group_state(new_state)
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def _tracking_states(self):
|
||||
"""States that the group is tracking."""
|
||||
states = []
|
||||
|
||||
for entity_id in self.tracking:
|
||||
state = self.hass.states.get(entity_id)
|
||||
|
||||
if state is not None:
|
||||
self._process_tracked_state(state)
|
||||
states.append(state)
|
||||
|
||||
def _state_changed_listener(self, entity_id, old_state, new_state):
|
||||
""" Listener to receive state changes of tracked entities. """
|
||||
self._process_tracked_state(new_state)
|
||||
self.update_ha_state()
|
||||
return states
|
||||
|
||||
def _process_tracked_state(self, tr_state):
|
||||
""" Updates group state based on a new state of a tracked entity. """
|
||||
def _update_group_state(self, tr_state=None):
|
||||
"""Update group state.
|
||||
|
||||
Optionally you can provide the only state changed since last update
|
||||
allowing this method to take shortcuts.
|
||||
"""
|
||||
# pylint: disable=too-many-branches
|
||||
# To store current states of group entities. Might not be needed.
|
||||
states = None
|
||||
gr_state, gr_on, gr_off = self._state, self.group_on, self.group_off
|
||||
|
||||
# We have not determined type of group yet
|
||||
if self.group_on is None:
|
||||
self.group_on, self.group_off = _get_group_on_off(tr_state.state)
|
||||
if gr_on is None:
|
||||
if tr_state is None:
|
||||
states = self._tracking_states
|
||||
|
||||
if self.group_on is not None:
|
||||
# New state of the group is going to be based on the first
|
||||
# state that we can recognize
|
||||
self._state = tr_state.state
|
||||
for state in states:
|
||||
gr_on, gr_off = \
|
||||
_get_group_on_off(state.state)
|
||||
if gr_on is not None:
|
||||
break
|
||||
else:
|
||||
gr_on, gr_off = _get_group_on_off(tr_state.state)
|
||||
|
||||
if gr_on is not None:
|
||||
self.group_on, self.group_off = gr_on, gr_off
|
||||
|
||||
# We cannot determine state of the group
|
||||
if gr_on is None:
|
||||
return
|
||||
|
||||
# There is already a group state
|
||||
cur_gr_state = self._state
|
||||
group_on, group_off = self.group_on, self.group_off
|
||||
if tr_state is None or (gr_state == gr_on and
|
||||
tr_state.state == gr_off):
|
||||
if states is None:
|
||||
states = self._tracking_states
|
||||
|
||||
# if cur_gr_state = OFF and tr_state = ON: set ON
|
||||
# if cur_gr_state = ON and tr_state = OFF: research
|
||||
# else: ignore
|
||||
if any(state.state == gr_on for state in states):
|
||||
self._state = gr_on
|
||||
else:
|
||||
self._state = gr_off
|
||||
|
||||
if cur_gr_state == group_off and tr_state.state == group_on:
|
||||
self._state = group_on
|
||||
elif tr_state.state in (gr_on, gr_off):
|
||||
self._state = tr_state.state
|
||||
|
||||
elif cur_gr_state == group_on and tr_state.state == group_off:
|
||||
if tr_state is None or self._assumed_state and \
|
||||
not tr_state.attributes.get(ATTR_ASSUMED_STATE):
|
||||
if states is None:
|
||||
states = self._tracking_states
|
||||
|
||||
# Set to off if no other states are on
|
||||
if not any(self.hass.states.is_state(ent_id, group_on)
|
||||
for ent_id in self.tracking
|
||||
if tr_state.entity_id != ent_id):
|
||||
self._state = group_off
|
||||
self._assumed_state = any(state.attributes.get(ATTR_ASSUMED_STATE)
|
||||
for state in states)
|
||||
|
||||
elif tr_state.attributes.get(ATTR_ASSUMED_STATE):
|
||||
self._assumed_state = True
|
||||
|
||||
@@ -7,17 +7,19 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/history/
|
||||
"""
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
from collections import defaultdict
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.components.recorder as recorder
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
|
||||
DOMAIN = 'history'
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
|
||||
SIGNIFICANT_DOMAINS = ('thermostat',)
|
||||
|
||||
URL_HISTORY_PERIOD = re.compile(
|
||||
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
|
||||
|
||||
@@ -35,6 +37,37 @@ def last_5_states(entity_id):
|
||||
return recorder.query_states(query, (entity_id, ))
|
||||
|
||||
|
||||
def get_significant_states(start_time, end_time=None, entity_id=None):
|
||||
"""Return states changes during UTC period start_time - end_time.
|
||||
|
||||
Significant states are all states where there is a state change,
|
||||
as well as all states from certain domains (for instance
|
||||
thermostat so that we get current temperature in our graphs).
|
||||
|
||||
"""
|
||||
where = """
|
||||
(domain in ({}) or last_changed=last_updated)
|
||||
AND last_updated > ?
|
||||
""".format(",".join(["'%s'" % x for x in SIGNIFICANT_DOMAINS]))
|
||||
|
||||
data = [start_time]
|
||||
|
||||
if end_time is not None:
|
||||
where += "AND last_updated < ? "
|
||||
data.append(end_time)
|
||||
|
||||
if entity_id is not None:
|
||||
where += "AND entity_id = ? "
|
||||
data.append(entity_id.lower())
|
||||
|
||||
query = ("SELECT * FROM states WHERE {} "
|
||||
"ORDER BY entity_id, last_updated ASC").format(where)
|
||||
|
||||
states = recorder.query_states(query, data)
|
||||
|
||||
return states_to_json(states, start_time, entity_id)
|
||||
|
||||
|
||||
def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
||||
"""
|
||||
Return states changes during UTC period start_time - end_time.
|
||||
@@ -55,20 +88,7 @@ def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
||||
|
||||
states = recorder.query_states(query, data)
|
||||
|
||||
result = defaultdict(list)
|
||||
|
||||
entity_ids = [entity_id] if entity_id is not None else None
|
||||
|
||||
# Get the states at the start time
|
||||
for state in get_states(start_time, entity_ids):
|
||||
state.last_changed = start_time
|
||||
result[state.entity_id].append(state)
|
||||
|
||||
# Append all changes to it
|
||||
for entity_id, group in groupby(states, lambda state: state.entity_id):
|
||||
result[entity_id].extend(group)
|
||||
|
||||
return result
|
||||
return states_to_json(states, start_time, entity_id)
|
||||
|
||||
|
||||
def get_states(utc_point_in_time, entity_ids=None, run=None):
|
||||
@@ -100,6 +120,33 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
|
||||
return recorder.query_states(query, where_data)
|
||||
|
||||
|
||||
def states_to_json(states, start_time, entity_id):
|
||||
"""Converts SQL results into JSON friendly data structure.
|
||||
|
||||
This takes our state list and turns it into a JSON friendly data
|
||||
structure {'entity_id': [list of states], 'entity_id2': [list of states]}
|
||||
|
||||
We also need to go back and create a synthetic zero data point for
|
||||
each list of states, otherwise our graphs won't start on the Y
|
||||
axis correctly.
|
||||
"""
|
||||
|
||||
result = defaultdict(list)
|
||||
|
||||
entity_ids = [entity_id] if entity_id is not None else None
|
||||
|
||||
# Get the states at the start time
|
||||
for state in get_states(start_time, entity_ids):
|
||||
state.last_changed = start_time
|
||||
state.last_updated = start_time
|
||||
result[state.entity_id].append(state)
|
||||
|
||||
# Append all changes to it
|
||||
for entity_id, group in groupby(states, lambda state: state.entity_id):
|
||||
result[entity_id].extend(group)
|
||||
return result
|
||||
|
||||
|
||||
def get_state(utc_point_in_time, entity_id, run=None):
|
||||
""" Return a state at a specific point in time. """
|
||||
states = get_states(utc_point_in_time, (entity_id,), run)
|
||||
@@ -152,4 +199,4 @@ def _api_history_period(handler, path_match, data):
|
||||
entity_id = data.get('filter_entity_id')
|
||||
|
||||
handler.write_json(
|
||||
state_changes_during_period(start_time, end_time, entity_id).values())
|
||||
get_significant_states(start_time, end_time, entity_id).values())
|
||||
|
||||
@@ -6,30 +6,31 @@ This module provides an API and a HTTP interface for debug purposes.
|
||||
For more details about the RESTful API, please refer to the documentation at
|
||||
https://home-assistant.io/developers/api/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import gzip
|
||||
from http import cookies
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from socketserver import ThreadingMixIn
|
||||
import ssl
|
||||
import threading
|
||||
import time
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from datetime import timedelta
|
||||
from http import cookies
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN,
|
||||
HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING,
|
||||
HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH,
|
||||
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED,
|
||||
HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_UNPROCESSABLE_ENTITY)
|
||||
import homeassistant.remote as rem
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
from homeassistant.const import (
|
||||
CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_ACCEPT_ENCODING,
|
||||
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONTENT_ENCODING,
|
||||
HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_EXPIRES,
|
||||
HTTP_HEADER_HA_AUTH, HTTP_HEADER_VARY, HTTP_METHOD_NOT_ALLOWED,
|
||||
HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_UNPROCESSABLE_ENTITY,
|
||||
SERVER_PORT)
|
||||
|
||||
DOMAIN = "http"
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ifttt/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
"""
|
||||
homeassistant.components.influxdb
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
InfluxDB component which allows you to send data to an Influx database.
|
||||
A component which allows you to send data to an Influx database.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/influxdb/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNKNOWN
|
||||
from homeassistant.helpers import state as state_helper
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF,
|
||||
STATE_UNLOCKED, STATE_LOCKED, STATE_UNKNOWN)
|
||||
from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
|
||||
STATE_BELOW_HORIZON)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,22 +19,27 @@ DEPENDENCIES = []
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 8086
|
||||
DEFAULT_DATABASE = 'home_assistant'
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
|
||||
REQUIREMENTS = ['influxdb==2.11.0']
|
||||
REQUIREMENTS = ['influxdb==2.12.0']
|
||||
|
||||
CONF_HOST = 'host'
|
||||
CONF_PORT = 'port'
|
||||
CONF_DB_NAME = 'database'
|
||||
CONF_USERNAME = 'username'
|
||||
CONF_PASSWORD = 'password'
|
||||
CONF_SSL = 'ssl'
|
||||
CONF_VERIFY_SSL = 'verify_ssl'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup the InfluxDB component. """
|
||||
|
||||
"""Setup the InfluxDB component."""
|
||||
from influxdb import InfluxDBClient, exceptions
|
||||
|
||||
if not validate_config(config, {DOMAIN: ['host']}, _LOGGER):
|
||||
if not validate_config(config, {DOMAIN: ['host',
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD]}, _LOGGER):
|
||||
return False
|
||||
|
||||
conf = config[DOMAIN]
|
||||
@@ -47,40 +49,35 @@ def setup(hass, config):
|
||||
database = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE)
|
||||
username = util.convert(conf.get(CONF_USERNAME), str)
|
||||
password = util.convert(conf.get(CONF_PASSWORD), str)
|
||||
ssl = util.convert(conf.get(CONF_SSL), bool, DEFAULT_SSL)
|
||||
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
|
||||
DEFAULT_VERIFY_SSL)
|
||||
|
||||
try:
|
||||
influx = InfluxDBClient(host=host, port=port, username=username,
|
||||
password=password, database=database)
|
||||
password=password, database=database,
|
||||
ssl=ssl, verify_ssl=verify_ssl)
|
||||
influx.query("select * from /.*/ LIMIT 1;")
|
||||
except exceptions.InfluxDBClientError as exc:
|
||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||
"check your entries in the configuration file and that"
|
||||
" the database exists and is READ/WRITE.", exc)
|
||||
"check your entries in the configuration file and that "
|
||||
"the database exists and is READ/WRITE.", exc)
|
||||
return False
|
||||
|
||||
def influx_event_listener(event):
|
||||
""" Listen for new messages on the bus and sends them to Influx. """
|
||||
|
||||
"""Listen for new messages on the bus and sends them to Influx."""
|
||||
state = event.data.get('new_state')
|
||||
|
||||
if state is None:
|
||||
if state is None or state.state in (STATE_UNKNOWN, ''):
|
||||
return
|
||||
|
||||
if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON):
|
||||
_state = 1
|
||||
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN,
|
||||
STATE_BELOW_HORIZON):
|
||||
_state = 0
|
||||
else:
|
||||
try:
|
||||
_state = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
_state = state.state
|
||||
if _state == '':
|
||||
return
|
||||
try:
|
||||
_state = float(_state)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
measurement = state.attributes.get('unit_of_measurement', state.domain)
|
||||
measurement = state.attributes.get('unit_of_measurement')
|
||||
if measurement in (None, ''):
|
||||
measurement = state.entity_id
|
||||
|
||||
json_body = [
|
||||
{
|
||||
|
||||
@@ -9,9 +9,9 @@ at https://home-assistant.io/components/input_boolean/
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'input_boolean'
|
||||
@@ -41,7 +41,7 @@ def turn_off(hass, entity_id):
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up input booleans."""
|
||||
""" Set up input boolean. """
|
||||
if not isinstance(config.get(DOMAIN), dict):
|
||||
_LOGGER.error('Expected %s config to be a dictionary', DOMAIN)
|
||||
return False
|
||||
@@ -68,7 +68,7 @@ def setup(hass, config):
|
||||
return False
|
||||
|
||||
def toggle_service(service):
|
||||
"""Handle a calls to the input boolean services."""
|
||||
""" Handle a calls to the input boolean services. """
|
||||
target_inputs = component.extract_from_service(service)
|
||||
|
||||
for input_b in target_inputs:
|
||||
@@ -86,10 +86,10 @@ def setup(hass, config):
|
||||
|
||||
|
||||
class InputBoolean(ToggleEntity):
|
||||
"""Represent a boolean input within Home Assistant."""
|
||||
""" Represent a boolean input. """
|
||||
|
||||
def __init__(self, object_id, name, state, icon):
|
||||
"""Initialize a boolean input."""
|
||||
""" Initialize a boolean input. """
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._state = state
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
homeassistant.components.input_select
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to offer a way to select an option from a list.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
at https://home-assistant.io/components/input_select/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'input_select'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = 'name'
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_ICON = 'icon'
|
||||
CONF_OPTIONS = 'options'
|
||||
|
||||
ATTR_OPTION = 'option'
|
||||
ATTR_OPTIONS = 'options'
|
||||
|
||||
SERVICE_SELECT_OPTION = 'select_option'
|
||||
|
||||
|
||||
def select_option(hass, entity_id, option):
|
||||
""" Set input_select to False. """
|
||||
hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_OPTION: option,
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Set up input select. """
|
||||
if not isinstance(config.get(DOMAIN), dict):
|
||||
_LOGGER.error('Expected %s config to be a dictionary', DOMAIN)
|
||||
return False
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
entities = []
|
||||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
if object_id != slugify(object_id):
|
||||
_LOGGER.warning("Found invalid key for boolean input: %s. "
|
||||
"Use %s instead", object_id, slugify(object_id))
|
||||
continue
|
||||
if not cfg:
|
||||
_LOGGER.warning("No configuration specified for %s", object_id)
|
||||
continue
|
||||
|
||||
name = cfg.get(CONF_NAME)
|
||||
options = cfg.get(CONF_OPTIONS)
|
||||
|
||||
if not isinstance(options, list) or len(options) == 0:
|
||||
_LOGGER.warning('Key %s should be a list of options', CONF_OPTIONS)
|
||||
continue
|
||||
|
||||
options = [str(val) for val in options]
|
||||
|
||||
state = cfg.get(CONF_INITIAL)
|
||||
|
||||
if state not in options:
|
||||
state = options[0]
|
||||
|
||||
icon = cfg.get(CONF_ICON)
|
||||
|
||||
entities.append(InputSelect(object_id, name, state, options, icon))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
|
||||
def select_option_service(call):
|
||||
""" Handle a calls to the input select services. """
|
||||
target_inputs = component.extract_from_service(call)
|
||||
|
||||
for input_select in target_inputs:
|
||||
input_select.select_option(call.data.get(ATTR_OPTION))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SELECT_OPTION,
|
||||
select_option_service)
|
||||
|
||||
component.add_entities(entities)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class InputSelect(Entity):
|
||||
""" Represent a select input. """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, object_id, name, state, options, icon):
|
||||
""" Initialize a select input. """
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._current_option = state
|
||||
self._options = options
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" If entity should be polled. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Name of the select input. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
""" Icon to be used for this entity. """
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" State of the component. """
|
||||
return self._current_option
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" State attributes. """
|
||||
return {
|
||||
ATTR_OPTIONS: self._options,
|
||||
}
|
||||
|
||||
def select_option(self, option):
|
||||
""" Select new option. """
|
||||
if option not in self._options:
|
||||
_LOGGER.warning('Invalid option: %s (possible options: %s)',
|
||||
option, ', '.join(self._options))
|
||||
return
|
||||
self._current_option = option
|
||||
self.update_ha_state()
|
||||
@@ -1,19 +1,20 @@
|
||||
"""
|
||||
homeassistant.components.insteon
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
homeassistant.components.insteon_hub
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Insteon Hub.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/insteon/
|
||||
https://home-assistant.io/components/insteon_hub/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_API_KEY, ATTR_DISCOVERED,
|
||||
ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED)
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DOMAIN = "insteon_hub"
|
||||
REQUIREMENTS = ['insteon_hub==0.4.5']
|
||||
|
||||
@@ -11,13 +11,12 @@ import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, EVENT_PLATFORM_DISCOVERED,
|
||||
EVENT_HOMEASSISTANT_STOP, ATTR_SERVICE, ATTR_DISCOVERED,
|
||||
ATTR_FRIENDLY_NAME)
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DOMAIN = "isy994"
|
||||
REQUIREMENTS = ['PyISY==1.0.5']
|
||||
@@ -147,7 +146,7 @@ class ISYDeviceABC(ToggleEntity):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns the state attributes for the node. """
|
||||
attr = {ATTR_FRIENDLY_NAME: self.name}
|
||||
attr = {}
|
||||
for name, prop in self._attrs.items():
|
||||
attr[name] = getattr(self, prop)
|
||||
attr = self._attr_filter(attr)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user