Compare commits
1863 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6797365d4f | |||
| 2410942fd0 | |||
| 2a75c6fcf6 | |||
| 159e287959 | |||
| a01111ae56 | |||
| 4599fd6dae | |||
| ddc139b853 | |||
| 2b6f0586b4 | |||
| 9904667a80 | |||
| 097ded73b1 | |||
| 5b2093ebc1 | |||
| 1fa2f23864 | |||
| 2e2537de2a | |||
| 1f8b66511d | |||
| 312d0320c6 | |||
| 284a1fd08e | |||
| 1fbf7afc6a | |||
| 025713ee41 | |||
| 81e5a852f0 | |||
| 4637a83c29 | |||
| ce82bd7515 | |||
| 534308e461 | |||
| 84bdba28b3 | |||
| 8af7858415 | |||
| 56ef16e858 | |||
| b9856a2af5 | |||
| 14a9b9fe3f | |||
| 47c4f66886 | |||
| 0d9b8a0d06 | |||
| c56701baaf | |||
| f22a40c3e8 | |||
| 986c9c55bf | |||
| 9838697d2b | |||
| d1256d4889 | |||
| d6eab03a61 | |||
| b534244e40 | |||
| d784610c52 | |||
| 8bff97083c | |||
| 3e4412d375 | |||
| 567727fb3b | |||
| 49ebc6d0b0 | |||
| fd5e2d1321 | |||
| 652f059d6a | |||
| cf7c2c492a | |||
| 7f1e181c9b | |||
| 08233da8a7 | |||
| 5423252a52 | |||
| 036cfe4ef6 | |||
| db6212dae3 | |||
| 391c6a358a | |||
| 29a651dd92 | |||
| 20dd782d9e | |||
| 9d76c58bd4 | |||
| c21918233b | |||
| 87008f23a1 | |||
| ba0a25aef6 | |||
| ee728345f6 | |||
| 2db873322d | |||
| 8ecced5fe6 | |||
| 897b5c668f | |||
| 4f536ac63d | |||
| 876978d64a | |||
| 91731c7234 | |||
| 1b8b2acb51 | |||
| 095dd70391 | |||
| 6879e39d6c | |||
| 7e8e91ef3c | |||
| ac43d23bd7 | |||
| cc7a4d545e | |||
| 40be9f4e0c | |||
| cd5b5c55e2 | |||
| 3a4250133a | |||
| 7035af6634 | |||
| fb7bd1bfe1 | |||
| f6bc1a4575 | |||
| 5ba9ac6465 | |||
| cfd65e48cb | |||
| b8a40457ee | |||
| 7ff9aecd4e | |||
| 032f06e015 | |||
| 6ac9210919 | |||
| b47f1c3a96 | |||
| 5222c19b4c | |||
| 1e97d31711 | |||
| 18f48191d9 | |||
| abc7243bc8 | |||
| 4c64be5cfa | |||
| 199375bd25 | |||
| 69bbfe5372 | |||
| d2c94da938 | |||
| 4e3c8a8697 | |||
| d75d08e97c | |||
| cd30f2de0d | |||
| bdad69307a | |||
| b16c17fae5 | |||
| 91ee889527 | |||
| 7b45001879 | |||
| fb46eff5f8 | |||
| 9701be9e9c | |||
| 605a572c86 | |||
| 1159360281 | |||
| 41214fd082 | |||
| d30fb7f04a | |||
| e57eca517b | |||
| 931dc27300 | |||
| d7094b996a | |||
| 9654f6b4e4 | |||
| c04555d7b1 | |||
| e755731588 | |||
| a687a3decb | |||
| 88c834fdd3 | |||
| feb1a15489 | |||
| 4d5e9f2931 | |||
| 1e3199eb8c | |||
| 17493660df | |||
| e59d6b7da0 | |||
| 2fd0b28c53 | |||
| bbd3a43585 | |||
| a02840379d | |||
| d641cd5eef | |||
| 6c518c74ec | |||
| fcbeafc6db | |||
| 4370f5170b | |||
| 49c6484f72 | |||
| ec642cbeff | |||
| 2823d8f0d8 | |||
| bfa579f8b2 | |||
| be04ebf9ed | |||
| 395a8663b5 | |||
| f9eb62dd9e | |||
| c6a6eaee9f | |||
| 87e23e7d73 | |||
| c586111c8c | |||
| e92c77a113 | |||
| 2d2b26ff1a | |||
| eed5b1e41b | |||
| 029094e549 | |||
| 28d80e6e2d | |||
| 5449d53e07 | |||
| 112b85877f | |||
| 0f6ec9b7ac | |||
| e246c83b0e | |||
| 3f4e34727d | |||
| a82931022c | |||
| 1974e2cfbe | |||
| 34b463f435 | |||
| e0ba33300a | |||
| 566c143ee3 | |||
| 014b11c61d | |||
| b70a455a13 | |||
| 9da8679dbd | |||
| c14ca9983c | |||
| 9c4fe6e7fe | |||
| a8edcfd315 | |||
| 0193454064 | |||
| 94633b3795 | |||
| 49982ac83c | |||
| 72940da874 | |||
| 5acf995d71 | |||
| f623a332cf | |||
| 3790764df9 | |||
| da2bcb3f27 | |||
| f5dd41e019 | |||
| 97edc39d0a | |||
| 2b4be33a3d | |||
| ac69db8133 | |||
| 5a126736e4 | |||
| 8e9c557a2c | |||
| d12dfc33a5 | |||
| 1be90081ef | |||
| 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 | |||
| 019af42e94 | |||
| 60d579af84 | |||
| c64da761f1 | |||
| 2d3d674295 | |||
| 590512916a | |||
| d398832112 | |||
| 88e7967a7d | |||
| e37c232bf6 | |||
| fb449cbc82 | |||
| 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 | |||
| d0bcec12b9 | |||
| 9fc62c1851 | |||
| 23d88cd4ad | |||
| 86f433067c | |||
| 106c53abf1 | |||
| 75232c43ce | |||
| b56369855a | |||
| 01a743c7d4 | |||
| 28f4283b40 | |||
| a41b66bb94 | |||
| 375faa9c91 | |||
| ef132e4583 | |||
| 05cbe54db3 | |||
| ae7697b900 | |||
| a8f7bc2324 | |||
| 9db0987e53 | |||
| 515307b404 | |||
| 03e7ac2a0e | |||
| c5cdf6d7cf | |||
| a3abd8bb08 | |||
| 5a7e380396 | |||
| 09ef2e1b8c | |||
| 80d2f35cc5 | |||
| ad57f27989 | |||
| a6720f54b3 | |||
| aaf75c7e45 | |||
| c5de42e7b5 | |||
| d6bb6a0777 | |||
| f87c7d6732 | |||
| 575e97a051 | |||
| 902077d78b | |||
| c17a4fca80 | |||
| 241a768983 | |||
| 72cca0a91a | |||
| dd7a7f4c75 | |||
| 468a8a1013 | |||
| 24d84dbb42 | |||
| 893a14e8db | |||
| cbc6323438 | |||
| d6db00b55a | |||
| eebb736bf8 | |||
| f192a15a8f | |||
| 9ec44fbe32 | |||
| ffc06e8bcb | |||
| 9c7b2ce9fd | |||
| 1de7dcdb5f | |||
| 9e7886b909 | |||
| 4045fb6862 | |||
| dda4f84150 | |||
| fda88f8fda | |||
| 3c05c8d1db | |||
| 21ec435430 | |||
| 24893bc28d | |||
| 403b9cbe3e | |||
| 8b0d19835c | |||
| c31522eea9 | |||
| ee33aa73e1 | |||
| 47498e4aa9 | |||
| ef4661f1e6 | |||
| b4070cfb78 | |||
| c750f16275 | |||
| 2248c271fa | |||
| 87b33d5098 | |||
| cf7c06d307 | |||
| 70d95cb6aa | |||
| 2c151c6db9 | |||
| 3fc5a60634 | |||
| 94d2f23cfc | |||
| 4bd102ddf5 | |||
| 232aa792f1 | |||
| ce9f76a0be | |||
| f3f592cdec | |||
| 1d2cd0811b | |||
| 886b581d2a | |||
| 88387d3123 | |||
| 3534c975f3 | |||
| 519abbbfa2 | |||
| b596fa33d6 | |||
| 14cd27aaa7 | |||
| 2e175b88bc | |||
| 537a7789fd | |||
| 80d1ab78dd | |||
| 821238f889 | |||
| 33a9ec0106 | |||
| a8f0f313c8 | |||
| 6796219f37 | |||
| 1dae22a465 | |||
| 7aba78f96e | |||
| 06cf07b097 | |||
| 271af2c608 | |||
| 8c6ce217e6 | |||
| 5410a0c8f6 | |||
| 1a422ecd5a | |||
| 6add5e387b | |||
| ca070a36e3 | |||
| 2c3a6e7905 | |||
| 0a4e857901 | |||
| d4b444823c | |||
| 2382dffbf4 | |||
| 33b0f4d05d | |||
| 5b6371ecda | |||
| 6f1f8ffea0 | |||
| 1f6f2de9c6 | |||
| b13e48bd71 | |||
| 9a79ecf2d3 | |||
| ebd475b380 | |||
| cff77a175d | |||
| 726637b867 | |||
| 9117fa6eb8 | |||
| 6ae57b5aaf | |||
| e8e2814313 | |||
| a918be517d | |||
| ec5d88b98e | |||
| 13fbefcdf8 | |||
| f4c9540a1b | |||
| e72f61ce73 | |||
| 4c056db3bb | |||
| c10a86d1bf | |||
| b97de5cef6 | |||
| 4233c0bc66 | |||
| b13008201e | |||
| 125ad8630d | |||
| 5c3ad5d4c0 | |||
| 414aa8563d | |||
| d08a181c72 | |||
| 80c6bf6744 | |||
| b1ba792715 | |||
| b99f6c1a46 | |||
| 1397f9e588 | |||
| bb7f92330d | |||
| 7cbd780748 | |||
| 784fea2d56 | |||
| 97f0425252 | |||
| 11120a8743 | |||
| 8009542b3e | |||
| 32cfa6998c | |||
| d10a5cf5e9 | |||
| 4089a7a0d3 | |||
| 5a926913d3 | |||
| 6c91831baa | |||
| 069dafa3a5 | |||
| 4abc5c97cd | |||
| bb3dd47088 | |||
| f6c53896e3 | |||
| ad2e2d916b | |||
| 8406f81811 | |||
| bcdfc555e0 | |||
| 3b89102338 | |||
| 60dd2d441d | |||
| 5830da63b1 | |||
| 50561ffe97 | |||
| 74e8446556 | |||
| d94db5388c | |||
| f66aeb2e73 | |||
| 54b82ecd91 | |||
| 2fa98167c2 | |||
| 57725136c0 | |||
| 40dbeb0b60 | |||
| ef92940ffb | |||
| e7865c1d67 | |||
| 02e634c6a2 | |||
| 0f937cad74 | |||
| 81dd1515ae | |||
| a65d0f0549 | |||
| 881c82c2df | |||
| df94c909f7 | |||
| a0ed469aa2 | |||
| afa4fc4ef5 | |||
| df450c3d1f | |||
| dc5d652d31 | |||
| e4fe19fff0 | |||
| f6f3f54228 | |||
| 6df67d2852 | |||
| 2411d1f2c8 | |||
| 9f6a1c75fa | |||
| 1842e5909e | |||
| 6f31aacb90 | |||
| e541b9ba77 | |||
| 53484e46a3 | |||
| de79a46d43 | |||
| bc19ef66bf | |||
| de08f0afaa | |||
| 706bbeae16 | |||
| e5497d89f4 | |||
| 048ec0aa66 | |||
| 03ed85b0a7 | |||
| 9d92707fd7 | |||
| 90c392e270 | |||
| 6cb6cbfefd | |||
| 6d2bca0fd1 | |||
| 99286391e1 | |||
| 711f2da496 | |||
| fbd68b6f89 | |||
| 85ebd0ab59 | |||
| 58034219b6 | |||
| 17f5a466d9 | |||
| 90ca6a0998 | |||
| ec2b433733 | |||
| 492c4b7f00 | |||
| b3beb9f3c9 | |||
| 837e7affa7 | |||
| 6d527842dd | |||
| e30915eb2c | |||
| 43e2b58f20 | |||
| d3c6c892a8 | |||
| a63edcf505 | |||
| 0e7088ce3b | |||
| 0042e7725d | |||
| 613f8d0bd2 | |||
| 61ca1ab2c1 | |||
| ad62591f43 | |||
| 5ba33bc40e | |||
| 143b08d661 | |||
| 87a9fd8252 | |||
| b1f7b5c6d7 | |||
| bb97af1504 | |||
| 9a092654e9 | |||
| d59b98ee2b | |||
| 6bbbbd9e17 | |||
| 9fbedd8b5f | |||
| a91163877f | |||
| 0acce86596 | |||
| c95b03f240 | |||
| 56ce3e5f5a | |||
| 8b5751ad44 | |||
| c615272c06 | |||
| a3b8122707 | |||
| 682e3460e0 | |||
| 71dbd10b39 | |||
| 7f143bcdf9 | |||
| f19a46dcfe | |||
| 625f69443a | |||
| 0bdd293572 | |||
| f8072aae68 | |||
| 92afcae9be | |||
| a6f37c032b | |||
| e66d15b71d | |||
| 1098194a89 | |||
| 66d23cd15f | |||
| c3a1193ef9 | |||
| ec2d9af8dc | |||
| f0e44728d7 | |||
| f67747456f | |||
| f6017a17b2 | |||
| 89aa3cbc62 | |||
| 543190dfb0 | |||
| fdc8c45a69 | |||
| d2d421ca8f | |||
| 7ad5b3a17b | |||
| c8dd9696b4 | |||
| 58ef69b95d | |||
| 0de9229d75 | |||
| f4c3ac2a62 | |||
| 6c5ceaf686 | |||
| 54f65ae87d | |||
| 4f2dc3cc2a | |||
| a4ee2bd8ef | |||
| d4629a7efe | |||
| 441ae73344 | |||
| 9249dc6dd3 | |||
| 4c4539caff | |||
| 4c0ff0e0d0 | |||
| 1ceee2d6c5 | |||
| cbb74d50ce | |||
| c2d72bbf09 | |||
| f97ba263c4 | |||
| 85df9e98bd | |||
| 2faafb9c0f | |||
| d43101f22a | |||
| 3d23cd10fc | |||
| 66cd8d264e | |||
| 291910d74e | |||
| 7ac648d0ab | |||
| 3b42390062 | |||
| a80917f530 | |||
| 8fbb585874 | |||
| 027d97321f | |||
| 62a1c9687e | |||
| a9c6f8c1d9 | |||
| 06b4fcc2cf | |||
| 6dc2501116 | |||
| 54060f27ef | |||
| 85aa4fdd2e | |||
| 0624445627 | |||
| 4ca4941c82 | |||
| 4dbd84ead0 | |||
| 0495776a22 | |||
| dd35551047 | |||
| 9f61369156 | |||
| bd536be66d | |||
| c5b69a0ee4 | |||
| 206b3a88a2 | |||
| 880bd011a9 | |||
| 7de91a270a | |||
| 09973abe8a | |||
| 342a819fd4 | |||
| 4bf185c868 | |||
| 78742c016b | |||
| 1ed314f6f6 | |||
| be23c6c86d | |||
| a194c4f1bd | |||
| edb24add6b | |||
| bc88985889 | |||
| 3d27dd3ec4 | |||
| c2f5eb3073 | |||
| 541b268721 | |||
| e77daed086 | |||
| fdbb409331 | |||
| f96c5aa62f | |||
| c07a096e57 | |||
| 48b6c5b5cb | |||
| d8d59d9a66 | |||
| 702dddbb2f | |||
| 127488004c | |||
| 7c925ac295 | |||
| 8617b92d1b | |||
| bb80e3a9fc | |||
| 80023f62d9 | |||
| 9617288bd5 | |||
| 4fd79afa42 | |||
| 719f9a63d9 | |||
| 4bc33d0352 | |||
| c4e1035638 | |||
| d40e889d3b | |||
| 41acc8fa43 | |||
| 90c2aed7b4 | |||
| 5108602de3 | |||
| 5ee17ffc58 | |||
| 6b899ddc1d | |||
| 0a711922ef | |||
| 475b631d9c | |||
| 40c75f0a51 | |||
| 1dd99a6d5d | |||
| bed9b038c8 | |||
| 9210c57c2d | |||
| 313cbda0aa | |||
| 86b0e49995 | |||
| a39148dd38 | |||
| b8c8c71b78 | |||
| 65a3bf2325 | |||
| ac34db3c8a | |||
| f2e86ecd8e | |||
| 7b993da0de | |||
| de5bee6359 | |||
| 34f124190c | |||
| d867366be1 | |||
| d0d375d433 | |||
| d17aa103b4 | |||
| f0af23a4f5 | |||
| bdd6bb7918 | |||
| 3ec49a0ef0 | |||
| 7a2d049ce3 | |||
| 7fba4b354e | |||
| 87cecd7e95 | |||
| 303cb8e350 | |||
| 236ae94474 | |||
| 308969e6dd | |||
| 4dd558a420 | |||
| 0e6a60b086 | |||
| 4fc0163139 | |||
| 314d34a644 | |||
| 2745b0f99e | |||
| c2e8646aed | |||
| e5919c1bfe | |||
| c8961fcf99 | |||
| 58cee75c0e | |||
| 4f13236008 | |||
| 2a377a6125 | |||
| 058dba50cc | |||
| e6846e7eb9 | |||
| a0ddda4bc6 | |||
| 57c0f96118 | |||
| a84429538b | |||
| 07953fb7e3 | |||
| 12da6f531e | |||
| 769f5aafb7 | |||
| a1abab8ced | |||
| 270a998e3c | |||
| d829497c3d | |||
| a8d5b0e5ec | |||
| 85d732a45a | |||
| 8f3e8d29f0 | |||
| ee4543d739 | |||
| 59456f20fb | |||
| 4a1f609893 | |||
| 36214c73ee | |||
| 20a1025a8c | |||
| ec85884d92 | |||
| 22c01b956f | |||
| 9cdf84dacf | |||
| 60f40800c4 | |||
| 3b7b12bbd5 | |||
| 9a1aad8e92 | |||
| 6f398f59df | |||
| 8ace656657 | |||
| a4481efe07 | |||
| dc31ddbef2 | |||
| 0c2fe4c5f3 | |||
| 03febb81d3 | |||
| 6f1a25d8d6 | |||
| f182f7dccd | |||
| 31fcd230b1 | |||
| 1ad5cae98e | |||
| 5af4864326 | |||
| ea8d278f8f | |||
| 88de9908ce | |||
| 1bbecce5eb | |||
| 2495226c87 | |||
| c1aa1fb0e0 | |||
| a5b198e2b8 | |||
| cd669239ae | |||
| bc73a6829d | |||
| 542b640ef0 | |||
| 7823fb9788 | |||
| bcb42674fc | |||
| 1f1a46a8bd | |||
| 7b999e6cd1 | |||
| c97a25cc86 | |||
| cdf24ec205 | |||
| 579f4d4cca | |||
| fce04e7ad0 | |||
| 6c94650603 | |||
| fca8ad5b0b | |||
| 4b4fb038e3 | |||
| 0b3a66dd1a | |||
| 2d8cf7de44 | |||
| ad931eac60 | |||
| 438e78610d | |||
| 058f6f9303 | |||
| 73cdf00512 | |||
| 12b5caed70 | |||
| 18d2668e3b | |||
| af21f72d17 | |||
| b64680e4a8 | |||
| 9faedf0e67 | |||
| 204c151113 | |||
| 9b8256ec07 | |||
| d61eb93c03 | |||
| a2c6cde83d | |||
| 825c91f0c3 | |||
| d406d7fa94 | |||
| 3db6faab4d | |||
| 15a046f20c | |||
| d3cd304f68 | |||
| 84fb96a42f | |||
| 5a1fed3980 | |||
| bb8af3a2d5 | |||
| 2803631906 | |||
| 4c47ed31ff | |||
| 1ed574b2a0 | |||
| d69c1b848a | |||
| 35c29dac3f | |||
| aac44f3a2b | |||
| 17dd8ddc9a | |||
| a8b36d9baa | |||
| 520a8d0d0d | |||
| 033bd30391 | |||
| 99e14380fe | |||
| 5576649d60 | |||
| 82cd2f4ed6 | |||
| 66f12afbb1 | |||
| 431656bbcf | |||
| a174a06e5c | |||
| 0d0fdb0adf | |||
| 5f8dd65acf | |||
| 601211f1d9 | |||
| 8983a97c70 | |||
| 2e899bd61c | |||
| d0a8a57ae4 | |||
| 08aabd18ad | |||
| 2478623ebc | |||
| 736183e6f5 | |||
| 31f2707b2f | |||
| d244d3b599 | |||
| 82904c59ce | |||
| f8b2570cb3 | |||
| 6268840b55 | |||
| c9ff0ab7eb | |||
| cf1f9a29bf | |||
| 48b1f8e137 | |||
| 4b96a7c820 | |||
| 452ebdac8e | |||
| 7dc1499386 | |||
| 3ccc7787af | |||
| 02879e79e6 | |||
| 305c87a9c9 | |||
| 3abc78eef2 | |||
| 4a421e25b0 | |||
| 86047eceb1 | |||
| 7edbb6aadc | |||
| 0361f37178 | |||
| 55c5d254d5 | |||
| 36f5caa214 | |||
| 8c7898ed05 | |||
| 39de92960d | |||
| 6d35bdafee | |||
| 217ffc215b | |||
| cbd3860585 | |||
| 5804dde0e9 | |||
| e9b2cf1600 | |||
| cdf2179b3e | |||
| 8f2ca856c7 | |||
| 622a6deb04 | |||
| a698bc477d | |||
| a36ae4b24a | |||
| c703c89dbd | |||
| b3227e491b | |||
| 9e83a80215 | |||
| 9c85702c87 | |||
| 9b7b39055d | |||
| 11f32d0500 | |||
| 326e26fbeb | |||
| 394c87c40b | |||
| ce152e9c94 | |||
| 9e0946b207 | |||
| da77edf8fc | |||
| 6cb991d583 | |||
| f8e5df237b | |||
| 5d953061e8 | |||
| 1bcca8cba1 | |||
| bdb6182921 | |||
| 55d1ad94ef | |||
| 6773c35760 | |||
| 5f89b34831 | |||
| 90ae5c6646 | |||
| a8bb75d070 | |||
| d82859b6ea | |||
| 7d41ce4e46 | |||
| c23375a18b | |||
| 4c4e5d5f47 | |||
| 6ff24ed047 | |||
| 69ed6fe6e7 | |||
| 4e2d75a8f4 | |||
| ae0dbbcfa5 | |||
| 25e1432403 | |||
| adfcfad488 | |||
| b0734e613f | |||
| 913c5ab47c | |||
| 429904c437 | |||
| 41a36df801 | |||
| 56a2ffca1d | |||
| 6e2fb17f19 | |||
| 586be7fad1 | |||
| d9b30d1421 | |||
| e9059a3ed9 | |||
| 473d6b1d05 | |||
| 7f17a50b4a | |||
| c7183a14a5 | |||
| c1eaf60461 | |||
| ca6b957839 | |||
| 680385df93 | |||
| 62f21c3ac6 | |||
| acfbbb3898 | |||
| 4403fe941d | |||
| 244fde880e | |||
| df99e4ef22 | |||
| 976d9f2d08 | |||
| f5dd146676 | |||
| 7b00b19223 | |||
| b114ba56ea | |||
| 0d32bd7a19 | |||
| 4feef3dd0d | |||
| 9c3b1b7a96 | |||
| a09d731058 | |||
| 27e35d5f34 | |||
| c8fa6cc127 | |||
| 468087e6bc | |||
| d4b6a7343f | |||
| 87c88078c8 | |||
| 8c1ebde1de | |||
| ab5a3f9de3 | |||
| eb3da8cb03 | |||
| 9e1ecd7124 | |||
| efbaf47dc7 | |||
| f10ecb2a8d | |||
| 6e81391711 | |||
| 0d64f4a2d5 | |||
| d63e5a60ae | |||
| 384f1344fd | |||
| add24915a3 | |||
| 089bbfc5cc | |||
| 1944b6a335 | |||
| b8cfe63fc6 | |||
| 9e091a2c92 | |||
| 41bcdc3356 | |||
| 40486676df | |||
| 6f2078cda3 | |||
| b8d2f2ba37 | |||
| 03491fcc09 | |||
| 571073fe1f | |||
| a577d5c5fa | |||
| 160814f425 | |||
| b83b36274a | |||
| f3db4306c2 | |||
| 5c7fb5d7ae | |||
| 0dfc1c4e7a | |||
| 3f151428b7 | |||
| 2606e4d641 | |||
| be25ea4f09 | |||
| 77959341a3 | |||
| 9f54bcc21b | |||
| 6d236b8169 | |||
| 09b894a4aa | |||
| 1e2b5e6991 | |||
| 496ec4bcca | |||
| 3e51d0b539 | |||
| c31a291a9c | |||
| 96938bb33c | |||
| e7b1682a4e | |||
| d191635fd8 | |||
| bdd945c1c4 | |||
| 6ccf63dae2 | |||
| 987282da78 | |||
| 56186232f3 | |||
| fb2da6be9a | |||
| 8796187389 | |||
| edff53609f | |||
| dbc91c1d48 | |||
| 0607c8f4b4 | |||
| 7ccb6b960c | |||
| 561a78bef3 | |||
| 9876a2a081 | |||
| fba5becd90 | |||
| d5179b4bdc | |||
| ff8f22854c | |||
| 110d721c76 | |||
| 8c010c8df4 | |||
| 0ac1759395 | |||
| 07fb4ff243 | |||
| 2650d235ea | |||
| 9e89197284 | |||
| 8159d36114 | |||
| cf4f4ce8c7 | |||
| 9a1883bb49 | |||
| 8e16a443e5 | |||
| 35411cd57e | |||
| bc4ab4d70a | |||
| 901f63be0c | |||
| 7ee71b1831 | |||
| 70a5704887 | |||
| 04316d9723 | |||
| 14246fe467 | |||
| 177590fd65 | |||
| 1c08923ffc | |||
| 7bb07cdc03 | |||
| 90c705354a | |||
| 55a236f912 | |||
| f071e3b4ac | |||
| a6a30c5f22 | |||
| dc8ac7386e | |||
| cc80ad00ff | |||
| 824dd60aef | |||
| 2efca59352 | |||
| 4a0ec36fea | |||
| 9bf8835190 | |||
| 411ce04d75 | |||
| 2e75f0b314 | |||
| dd60cc020e | |||
| 9dca22aa27 | |||
| b5fc7f5e71 | |||
| 80d3552960 | |||
| 845926236e | |||
| eae07c070a | |||
| 659226886f | |||
| a0ff8819a9 | |||
| 4196d6a21c | |||
| c564c73617 | |||
| 5dabba3941 | |||
| 257743facc | |||
| 0369f5153e | |||
| 04aa4ff3a8 | |||
| 38c529502d | |||
| dbad01f73d | |||
| 9087e6a0d0 | |||
| faff14ac94 | |||
| f2aeb2a6f9 | |||
| 56b38e64ae | |||
| eb4f0e4ae9 | |||
| 1747db1888 | |||
| 5066e6a183 | |||
| 8eb73c8f17 | |||
| 3ce6463af3 | |||
| cceb79a378 | |||
| 2a30c07429 | |||
| f246508c9a | |||
| eb6b4218fc | |||
| 7fb5927ac8 | |||
| 4eec89b35e | |||
| 4c33eba378 | |||
| fe2ae16210 | |||
| ab8ff42cdd | |||
| 687ce64551 | |||
| 31b4c8eb4d | |||
| f6d16ba118 | |||
| 69d8b995d4 | |||
| 70bc6f4506 | |||
| afbf377b6d | |||
| da63800319 | |||
| 0075752c06 | |||
| 0a44750441 | |||
| c91cd9aa84 | |||
| c8234ffb8d | |||
| 14056951c7 | |||
| e76defe035 | |||
| 3b8c5d6833 | |||
| 956a6418cc | |||
| 507145e954 | |||
| 40c2e10756 | |||
| 58988787ea | |||
| 79a1ffac9c | |||
| 4f6243fe0e | |||
| 2a3e086410 | |||
| 0e9c51a479 | |||
| d6dad8c818 | |||
| 17940f36e8 | |||
| 149449624e | |||
| 574de3b1b6 | |||
| 85873390ee | |||
| 4e6b01b0b9 | |||
| 5ecdd06bd5 | |||
| 9404636536 | |||
| a764817ee9 | |||
| dade33187f | |||
| b3c13747dc | |||
| a3f805d88e | |||
| 66fca475c6 | |||
| 9fa8b27d65 | |||
| 455a9c83a6 | |||
| 94eb002b0a | |||
| fcdaa923c5 | |||
| a35dcf860e | |||
| 4029d149fb | |||
| 027b891052 | |||
| 2e0042adb0 | |||
| 79c92cd0c4 | |||
| 946f6cab9d | |||
| 9e0d19df6c | |||
| 6d38288411 | |||
| 056645fe13 | |||
| d16526739c | |||
| 6a4442cddf | |||
| c1d728ce00 | |||
| a517784c9e | |||
| 91a945f4c7 | |||
| cec62bdf87 | |||
| 53239387e0 | |||
| a1009d9138 | |||
| e821b546d5 | |||
| 600831cff5 | |||
| 87d40f6673 | |||
| 5755c6f593 | |||
| 3ed69cec68 | |||
| df24a1bfa7 | |||
| 90f0632a69 | |||
| 3faa1a4393 | |||
| f93d6a1a11 | |||
| 0f777ecd4c | |||
| cc962c6bb2 | |||
| 9833b4b663 | |||
| b3171c7cde | |||
| 4528c57539 | |||
| 7e9eaf14b8 | |||
| 035d518cb6 | |||
| 85b62a20c8 | |||
| 46f742f82f | |||
| 729c24d59b | |||
| f73f824e0e | |||
| 360b99be59 | |||
| 931f7e8615 | |||
| 106a229a27 | |||
| 649ce7befb | |||
| ed9b75756a | |||
| 27c5c1cb9f | |||
| 1d017beb93 | |||
| 4286e116f3 | |||
| 64e3db2444 | |||
| f458484770 | |||
| 01c4038dec | |||
| 27e3e72211 | |||
| a84ff14b00 | |||
| ab9ab532c8 | |||
| 9fc0d93e44 | |||
| ded99d7480 | |||
| 2b975c8620 | |||
| 9eea7a6cde | |||
| b1bf6a609e | |||
| 5c63862054 | |||
| 0b325b2b7d | |||
| 1c54111018 | |||
| 13b0d2afa3 | |||
| a10908e167 | |||
| 56bb9f2da0 | |||
| 2368e5f4ec | |||
| 84226b573e | |||
| e19af76c70 | |||
| 9a9ecb5916 | |||
| d55fda28c2 | |||
| 7acef84aad | |||
| 5f8fc7e197 | |||
| 0dcc69d800 | |||
| 47b5fbfaf3 | |||
| d1383ac94d | |||
| af09a305cf | |||
| b440c260e6 | |||
| de68d3355a | |||
| fc8c26005c | |||
| 452348a7f9 | |||
| d3a21bee82 | |||
| 5ff6eb8b9c | |||
| 5d3d2d4110 | |||
| 3938b9f3f6 | |||
| 48593c6a68 | |||
| 1e52d5c7f2 | |||
| d84bea3621 | |||
| 9e41c495fc | |||
| c423a51315 | |||
| 9f396b44f7 | |||
| ee339cfb20 | |||
| fe485cc27d | |||
| e1990e07c7 | |||
| 9463c84603 | |||
| 7cc707f1ce | |||
| 0d4f681a4e | |||
| e60dce9712 | |||
| aff82da611 | |||
| 2c157e6885 | |||
| aaca0a64cb | |||
| 48c3bbd5ac | |||
| 39e3a3c463 | |||
| fd9da7f9de | |||
| 98467d0d9f | |||
| 832674286b | |||
| 9d8e077acc | |||
| b33e9fe6d9 | |||
| f4238ca242 | |||
| 301194034e | |||
| b8de2c5ebf | |||
| 527d30ae4a | |||
| 535a3c399e | |||
| e0c4c8d7dd | |||
| d9b8ab8851 | |||
| b2ae365558 | |||
| 1cb4d40bbb | |||
| b7712ac682 | |||
| a833e8b413 | |||
| e112620b2d | |||
| 934b097bc7 | |||
| f8668075d0 | |||
| 03962ab6ae | |||
| facbbabe58 | |||
| 59524c7933 | |||
| 1d141566bd | |||
| c4fe480b7b | |||
| 705e3e4fbb | |||
| eefa62748b | |||
| be14499bca | |||
| f582137fe3 | |||
| 254889e3fd | |||
| 9ecc08c0c8 | |||
| e3d4e3ad4d | |||
| b6342ed848 | |||
| 0383dddae7 | |||
| ac41f3028c | |||
| 750ca79ac0 | |||
| d0ae22428d | |||
| 107994f3ac | |||
| 853aa2ac42 | |||
| 7985468aba | |||
| 502184812d | |||
| 02a8ce71d0 | |||
| 08de7d954a | |||
| 71bf707bcf | |||
| 9d8865ad4d | |||
| c3f0d618be | |||
| 21ee621aec | |||
| 31f1e1d7a4 | |||
| dd0424435a | |||
| 0555294afe | |||
| 28bbf39155 | |||
| 587ef04560 | |||
| 38f28fe24b | |||
| 1ce81ee6db | |||
| 228142e497 | |||
| 361ab0f92b | |||
| e68a8f9c0f | |||
| 15770ff90f | |||
| 618ebfe43c | |||
| ff15fea9f8 | |||
| 2872c89f0c | |||
| 2dc9bc98f7 | |||
| 72d7e6e9dd | |||
| f912daf4b2 | |||
| 9bd11205fb | |||
| 0025e67b05 | |||
| 5e2641e288 | |||
| f173f8b88b | |||
| d9fc2a8bf6 | |||
| d6cac08be6 | |||
| 2e89f0a8c7 | |||
| 758c0aae24 | |||
| 8fee38f2cd | |||
| 41ab635dcd | |||
| 48a424e86f | |||
| 6113988ccd | |||
| 2732b20305 | |||
| ac59847120 | |||
| b53993e8c0 | |||
| 90eab17ea6 | |||
| 5b4f607da1 | |||
| 72ebb22eba | |||
| dc2f3861c9 | |||
| 6a25af4a9f | |||
| 6e0c30d9f7 | |||
| 022d6af6fc | |||
| 32003daf3f | |||
| 5fba67f6c3 | |||
| a3981be501 | |||
| 343b5bd2b8 | |||
| 35f1810c74 | |||
| 7ba9fb90f1 | |||
| 5023772b3e | |||
| ee805ec145 | |||
| a301d869d7 | |||
| cb0eb2df7d | |||
| 18ca7b4f9e | |||
| aff1c27372 | |||
| 01203c7c4c | |||
| 8841eef2b7 | |||
| 52b39efc51 | |||
| 7d503e3f8b | |||
| 73e3ce5044 | |||
| f4c3fbe8fd | |||
| 41a0f2c198 | |||
| 0016ff6acc | |||
| 45bd371cbf | |||
| 70698f7ab0 | |||
| 286299c4c9 | |||
| f76edf0ed9 | |||
| 733de6b357 | |||
| e67732b4f8 | |||
| 546377e80a | |||
| 0df39b4df5 | |||
| 64ebe8c6d0 | |||
| 78cfed1fb0 | |||
| 99aa4307ef | |||
| ef394b8af7 | |||
| aebab47fff | |||
| f2d553bfd1 | |||
| e84ef2d2d7 | |||
| 957b09707d | |||
| 9ecfc41e09 | |||
| 194e7f8811 | |||
| 6a021c9ef6 | |||
| b6d7cacc61 | |||
| e6fdcc94e6 | |||
| 05978ad88d | |||
| 1146d6e58d | |||
| 6809a881fa | |||
| 9f01d7abca | |||
| bbfeba0fe4 | |||
| ad3f96fa25 | |||
| d91fe792c5 | |||
| 93c86ca5a1 | |||
| bbf73e0bae | |||
| 47eb4e76cf | |||
| ada612e09d | |||
| 00f0dfb971 | |||
| 7204bc018f | |||
| 7224775aa8 | |||
| 60460e8217 | |||
| 67bcb00c9e | |||
| 74b37bd61b | |||
| 08da1e6f8d | |||
| fabd0ced3f | |||
| 2861bbb02c | |||
| 4c3d6981a6 | |||
| 341c3a8fcd | |||
| 69e9d39690 | |||
| 65f3e7ccf4 | |||
| 067011af15 | |||
| 271c2f6537 | |||
| f2f598bd26 | |||
| 4f75286f64 | |||
| c4d4dcccb7 | |||
| 128e3bb762 | |||
| a33220db7f | |||
| 4bd0db30c9 | |||
| e60ab8f4c2 | |||
| 38b564e413 | |||
| 76ac913fc0 | |||
| 0dbf4b3c10 | |||
| 08ba71a359 | |||
| 1bebb17e9f | |||
| e45ef0013b | |||
| 9dfa4ea233 | |||
| a8ddae9343 | |||
| 3e60c4801c | |||
| bc106bcb10 | |||
| 6f3cebdacf | |||
| b0fa80ad4c | |||
| 067b5862c0 | |||
| 351430c1b3 | |||
| e001ea913a | |||
| 901b9075dc | |||
| d141306493 | |||
| 6c12580b69 | |||
| 0d3a099926 | |||
| 09a82dedf0 | |||
| 80e829f53a | |||
| 27bc4c582b | |||
| cc196d9888 | |||
| 68ff9dd74f | |||
| d4b3af327d | |||
| 8269e843f2 | |||
| 7acb3dffe4 | |||
| 1bda0bd73b | |||
| f170799182 | |||
| d4f0f0ffd3 | |||
| 7f1254d750 | |||
| 100400f149 | |||
| 90681c2dc9 | |||
| dbcd055cfe | |||
| d6feb82f9b | |||
| fc0e76764d | |||
| 64c9619d75 | |||
| 8074854731 | |||
| fff6b24449 | |||
| 396a65ab03 | |||
| ee73bc9ad4 | |||
| 8dc0de1d05 | |||
| 715abf241e | |||
| f37d0d1c20 | |||
| 0621260435 | |||
| 427944cc44 | |||
| 422a93e735 | |||
| b4ee3f73b4 | |||
| 44abc31057 | |||
| 820b2a31b3 | |||
| 08d29d3630 | |||
| 44714614ad | |||
| 065f4b7c20 | |||
| 030686a978 | |||
| 799043dc0a | |||
| 105dc2847e | |||
| b4cf0e874a | |||
| 3df6c584c0 | |||
| 3493db1052 | |||
| d254e7e9e5 | |||
| 52344d65bc | |||
| a6006b1835 | |||
| b9730e6914 | |||
| 037bca041e | |||
| 77a1a1529c | |||
| fa7391cdf6 | |||
| d25f58e650 | |||
| 6d315a1a7c | |||
| 685964785d | |||
| ca32c81612 | |||
| 5f89db5957 | |||
| 4eacea32da | |||
| d05af62680 | |||
| f1fed78992 | |||
| 8df32aac3c | |||
| 18d0f4461f | |||
| c6d1a4bdaf | |||
| ab9e173179 | |||
| 22fcbc67cf | |||
| e317e0798b | |||
| c78899c4f3 | |||
| bca65b620a | |||
| e92fe149fe | |||
| 377d2c6e5a | |||
| 8be53af78f | |||
| 573dfb648f | |||
| c60bb35d4a | |||
| 90007a04d3 | |||
| 45fe37a301 | |||
| ef76047ba2 | |||
| d475e5362b | |||
| 3027b4a5a8 | |||
| 3829abbd2d | |||
| e6e3b37a62 | |||
| ce501ae627 | |||
| b6f954e082 | |||
| b368388714 | |||
| 8842e4e94f | |||
| 0ed608abff | |||
| afcf3eaac3 |
+77
-20
@@ -5,9 +5,21 @@ omit =
|
||||
homeassistant/__main__.py
|
||||
|
||||
# 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
|
||||
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
@@ -15,45 +27,76 @@ omit =
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
homeassistant/components/*/tellstick.py
|
||||
|
||||
homeassistant/components/tellduslive.py
|
||||
homeassistant/components/*/tellduslive.py
|
||||
|
||||
homeassistant/components/*/vera.py
|
||||
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/verisure.py
|
||||
homeassistant/components/*/verisure.py
|
||||
|
||||
homeassistant/components/wemo.py
|
||||
homeassistant/components/*/wemo.py
|
||||
|
||||
homeassistant/components/wink.py
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zwave.py
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/mysensors.py
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
homeassistant/components/nest.py
|
||||
homeassistant/components/*/nest.py
|
||||
|
||||
homeassistant/components/rfxtrx.py
|
||||
homeassistant/components/*/rfxtrx.py
|
||||
|
||||
homeassistant/components/ifttt.py
|
||||
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
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/geofancy.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/owntracks.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
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/keyboard.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/mqtt.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
@@ -61,49 +104,63 @@ omit =
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/samsungtv.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/notify/file.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/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/command_sensor.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
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/mysensors.py
|
||||
homeassistant/components/sensor/netatmo.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/rpi_gpio.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/command_switch.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_gpio.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wemo.py
|
||||
homeassistant/components/thermostat/honeywell.py
|
||||
homeassistant/components/thermostat/nest.py
|
||||
homeassistant/components/thermostat/heatmiser.py
|
||||
homeassistant/components/thermostat/homematic.py
|
||||
homeassistant/components/thermostat/proliphix.py
|
||||
homeassistant/components/thermostat/radiotherm.py
|
||||
|
||||
|
||||
|
||||
@@ -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,29 @@
|
||||
**Description:**
|
||||
|
||||
|
||||
**Related issue (if applicable):** #
|
||||
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
|
||||
- [ ] Local tests with `tox` run successfully.
|
||||
- [ ] TravisCI does not fail. **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.
|
||||
- [ ] Commits have been [squashed][squash].
|
||||
- If code communicates with devices:
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
- [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New files were added to `.coveragerc`.
|
||||
- If the code does not interact with devices:
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
[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
|
||||
|
||||
+11
@@ -41,6 +41,7 @@ Icon
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
.eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
@@ -68,6 +69,16 @@ nosetests.xml
|
||||
|
||||
.python-version
|
||||
|
||||
# emacs auto backups
|
||||
*~
|
||||
*#
|
||||
*.orig
|
||||
|
||||
# venv stuff
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv
|
||||
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
+16
-8
@@ -1,11 +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/virtualenv/python3.4.2/
|
||||
python:
|
||||
- "3.4"
|
||||
install:
|
||||
- script/bootstrap_server
|
||||
script:
|
||||
- script/cibuild
|
||||
- $HOME/.cache/pip
|
||||
install: pip install -U tox coveralls
|
||||
language: python
|
||||
script: tox
|
||||
after_success: coveralls
|
||||
|
||||
+21
-6
@@ -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,13 +17,13 @@ For help on building your component, please see the [developer documentation](ht
|
||||
|
||||
After you finish adding support for your device:
|
||||
|
||||
- Add a link to the website of your device/service/component in the "examples" listing of the `README.md` file.
|
||||
- Add any new dependencies to `requirements_all.txt` if needed. There is no ordering right now, so just add it to the end of the file.
|
||||
- Update the `.coveragerc` file to exclude your platform if there are no tests available.
|
||||
- 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 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:
|
||||
|
||||
@@ -67,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.
|
||||
|
||||
```bash
|
||||
> 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.
|
||||
|
||||
+16
-8
@@ -1,19 +1,27 @@
|
||||
FROM python:3-onbuild
|
||||
FROM python:3.4
|
||||
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
|
||||
VOLUME /config
|
||||
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN pip3 install --no-cache-dir colorlog cython
|
||||
|
||||
# For the nmap tracker
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nmap net-tools && \
|
||||
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y cython3 libudev-dev && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
|
||||
pip3 install "cython<0.23" && \
|
||||
script/build_python_openzwave
|
||||
COPY script/build_python_openzwave script/build_python_openzwave
|
||||
RUN script/build_python_openzwave && \
|
||||
mkdir -p /usr/local/share/python-openzwave && \
|
||||
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
|
||||
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Paulus Schoutsen
|
||||
Copyright (c) 2016 Paulus Schoutsen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
include README.md
|
||||
include README.rst
|
||||
include LICENSE
|
||||
graft homeassistant
|
||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# Home Assistant [](https://travis-ci.org/balloob/home-assistant) [](https://coveralls.io/r/balloob/home-assistant?branch=master) [](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
[demo]: https://home-assistant.io/demo/
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control.
|
||||
|
||||
To get started:
|
||||
```bash
|
||||
python3 -m pip install homeassistant
|
||||
hass --open-ui
|
||||
```
|
||||
|
||||
Check out [the website](https://home-assistant.io) for [a demo][demo], installation instructions, tutorials and documentation.
|
||||
|
||||
[][demo]
|
||||
|
||||
Examples of devices it can interface it:
|
||||
|
||||
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) and any SNMP capable Linksys WAP/WRT
|
||||
* [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
|
||||
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Plex](https://plex.tv/), [Kodi (XBMC)](http://kodi.tv/), iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)), and Amazon Fire TV (by way of [python-firetv](https://github.com/happyleavesaoc/python-firetv))
|
||||
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [RFXtrx](http://www.rfxcom.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
|
||||
* Interaction with [IFTTT](https://ifttt.com/)
|
||||
* Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org).
|
||||
* [See full list of supported devices](https://home-assistant.io/components/)
|
||||
|
||||
Built home automation on top of your devices:
|
||||
|
||||
* Keep a precise history of every change to the state of your house
|
||||
* Turn on the lights when people get home after sun set
|
||||
* Turn on lights slowly during sun set to compensate for less light
|
||||
* Turn off all lights and devices when everybody leaves the house
|
||||
* Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects like [OwnTracks](http://owntracks.org/)
|
||||
* Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), [Telegram](https://telegram.org/), and [Jabber (XMPP)](http://xmpp.org)
|
||||
|
||||
The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html).
|
||||
|
||||
If you run into issues while using Home Assistant or during development of a component, check the [Home Assistant help section](https://home-assistant.io/help/) how to reach us.
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/balloob/home-assistant|
|
||||
===========================================================================================================
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. The
|
||||
goal of Home Assistant is to be able to track and control all devices at
|
||||
home and offer a platform for automating control.
|
||||
|
||||
To get started:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python3 -m pip install homeassistant
|
||||
hass --open-ui
|
||||
|
||||
Check out `the website <https://home-assistant.io>`__ for `a
|
||||
demo <https://home-assistant.io/demo/>`__, installation instructions,
|
||||
tutorials and documentation.
|
||||
|
||||
|screenshot-states|
|
||||
|
||||
Examples of devices it can interface it:
|
||||
|
||||
- Monitoring connected devices to a wireless router:
|
||||
`OpenWrt <https://openwrt.org/>`__,
|
||||
`Tomato <http://www.polarcloud.com/tomato>`__,
|
||||
`Netgear <http://netgear.com>`__,
|
||||
`DD-WRT <http://www.dd-wrt.com/site/index>`__,
|
||||
`TPLink <http://www.tp-link.us/>`__,
|
||||
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__ and any SNMP
|
||||
capable Linksys WAP/WRT
|
||||
- `Philips Hue <http://meethue.com>`__ lights,
|
||||
`WeMo <http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/>`__
|
||||
switches, `Edimax <http://www.edimax.com/>`__ switches,
|
||||
`Efergy <https://efergy.com>`__ energy monitoring, and
|
||||
`Tellstick <http://www.telldus.se/products/tellstick>`__ devices and
|
||||
sensors
|
||||
- `Google
|
||||
Chromecasts <http://www.google.com/intl/en/chrome/devices/chromecast>`__,
|
||||
`Music Player Daemon <http://www.musicpd.org/>`__, `Logitech
|
||||
Squeezebox <https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29>`__,
|
||||
`Plex <https://plex.tv/>`__, `Kodi (XBMC) <http://kodi.tv/>`__,
|
||||
iTunes (by way of
|
||||
`itunes-api <https://github.com/maddox/itunes-api>`__), and Amazon
|
||||
Fire TV (by way of
|
||||
`python-firetv <https://github.com/happyleavesaoc/python-firetv>`__)
|
||||
- Support for
|
||||
`ISY994 <https://www.universal-devices.com/residential/isy994i-series/>`__
|
||||
(Insteon and X10 devices), `Z-Wave <http://www.z-wave.com/>`__, `Nest
|
||||
Thermostats <https://nest.com/>`__,
|
||||
`RFXtrx <http://www.rfxcom.com/>`__,
|
||||
`Arduino <https://www.arduino.cc/>`__, `Raspberry
|
||||
Pi <https://www.raspberrypi.org/>`__, and
|
||||
`Modbus <http://www.modbus.org/>`__
|
||||
- Interaction with `IFTTT <https://ifttt.com/>`__
|
||||
- Integrate data from the `Bitcoin <https://bitcoin.org>`__ network,
|
||||
meteorological data from
|
||||
`OpenWeatherMap <http://openweathermap.org/>`__ and
|
||||
`Forecast.io <https://forecast.io/>`__,
|
||||
`Transmission <http://www.transmissionbt.com/>`__, or
|
||||
`SABnzbd <http://sabnzbd.org>`__.
|
||||
- `See full list of supported
|
||||
devices <https://home-assistant.io/components/>`__
|
||||
|
||||
Built home automation on top of your devices:
|
||||
|
||||
- Keep a precise history of every change to the state of your house
|
||||
- Turn on the lights when people get home after sun set
|
||||
- Turn on lights slowly during sun set to compensate for less light
|
||||
- Turn off all lights and devices when everybody leaves the house
|
||||
- Offers a `REST API <https://home-assistant.io/developers/api/>`__
|
||||
and can interface with MQTT for easy integration with other projects
|
||||
like `OwnTracks <http://owntracks.org/>`__
|
||||
- Allow sending notifications using
|
||||
`Instapush <https://instapush.im>`__, `Notify My Android
|
||||
(NMA) <http://www.notifymyandroid.com/>`__,
|
||||
`PushBullet <https://www.pushbullet.com/>`__,
|
||||
`PushOver <https://pushover.net/>`__, `Slack <https://slack.com/>`__,
|
||||
`Telegram <https://telegram.org/>`__, and `Jabber
|
||||
(XMPP) <http://xmpp.org>`__
|
||||
|
||||
The system is built modular so support for other devices or actions can
|
||||
be implemented easily. See also the `section on
|
||||
architecture <https://home-assistant.io/developers/architecture.html>`__
|
||||
and the `section on creating your own
|
||||
components <https://home-assistant.io/developers/creating_components.html>`__.
|
||||
|
||||
If you run into issues while using Home Assistant or during development
|
||||
of a component, check the `Home Assistant help
|
||||
section <https://home-assistant.io/help/>`__ how to reach us.
|
||||
|
||||
.. |Build Status| image:: https://travis-ci.org/balloob/home-assistant.svg?branch=master
|
||||
:target: https://travis-ci.org/balloob/home-assistant
|
||||
.. |Coverage Status| image:: https://img.shields.io/coveralls/balloob/home-assistant.svg
|
||||
:target: https://coveralls.io/r/balloob/home-assistant?branch=master
|
||||
.. |Join the chat at https://gitter.im/balloob/home-assistant| image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
:target: https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |screenshot-states| image:: https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png
|
||||
:target: https://home-assistant.io/demo/
|
||||
@@ -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
|
||||
@@ -29,22 +28,29 @@ import time
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.event_decorators import \
|
||||
track_state_change, track_time_change
|
||||
from homeassistant.helpers.service import service
|
||||
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']
|
||||
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'
|
||||
|
||||
# Name of the service that we expose
|
||||
# Variable for storing configuration parameters.
|
||||
TARGET_ID = None
|
||||
|
||||
# Name of the service that we expose.
|
||||
SERVICE_FLASH = 'flash'
|
||||
|
||||
# Shortcut for the logger
|
||||
@@ -52,85 +58,92 @@ _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]
|
||||
TARGET_ID = config[DOMAIN][CONF_TARGET]
|
||||
|
||||
# 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)
|
||||
# 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)
|
||||
|
||||
# Tell the bootstrapper that we failed to initialize
|
||||
# Tell the bootstrapper that we failed to initialize and clear the
|
||||
# stored target id so our functions don't run.
|
||||
TARGET_ID = None
|
||||
return False
|
||||
|
||||
# We will use the component helper methods to check the states.
|
||||
device_tracker = loader.get_component('device_tracker')
|
||||
light = loader.get_component('light')
|
||||
|
||||
def track_devices(entity_id, old_state, new_state):
|
||||
""" Called when the group.all devices change state. """
|
||||
|
||||
# If anyone comes home and the core is not on, turn it on.
|
||||
if new_state.state == STATE_HOME and not core.is_on(hass, target_id):
|
||||
|
||||
core.turn_on(hass, target_id)
|
||||
|
||||
# If all people leave the house and the core 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)
|
||||
|
||||
# Register our track_devices method to receive state changes of the
|
||||
# all tracked devices group.
|
||||
hass.states.track_change(
|
||||
device_tracker.ENTITY_ID_ALL_DEVICES, track_devices)
|
||||
|
||||
def wake_up(now):
|
||||
""" Turn it on in the morning if there are people home and
|
||||
it is not already on. """
|
||||
|
||||
if device_tracker.is_on(hass) and not core.is_on(hass, target_id):
|
||||
_LOGGER.info('People home at 7AM, turning it on')
|
||||
core.turn_on(hass, target_id)
|
||||
|
||||
# Register our wake_up service to be called at 7AM in the morning
|
||||
hass.track_time_change(wake_up, hour=7, minute=0, second=0)
|
||||
|
||||
def all_lights_off(entity_id, old_state, new_state):
|
||||
""" If all lights turn off, turn off. """
|
||||
|
||||
if core.is_on(hass, target_id):
|
||||
_LOGGER.info('All lights have been turned off, turning it off')
|
||||
core.turn_off(hass, target_id)
|
||||
|
||||
# Register our all_lights_off method to be called when all lights turn off
|
||||
hass.states.track_change(
|
||||
light.ENTITY_ID_ALL_LIGHTS, all_lights_off, STATE_ON, STATE_OFF)
|
||||
|
||||
def flash_service(call):
|
||||
""" Service that will turn the target off for 10 seconds
|
||||
if on and vice versa. """
|
||||
|
||||
if core.is_on(hass, target_id):
|
||||
core.turn_off(hass, target_id)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_on(hass, target_id)
|
||||
|
||||
else:
|
||||
core.turn_on(hass, target_id)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_off(hass, target_id)
|
||||
|
||||
# Register our service with HASS.
|
||||
hass.services.register(DOMAIN, SERVICE_FLASH, flash_service)
|
||||
|
||||
# Tells the bootstrapper that the component was successfully initialized
|
||||
# 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."""
|
||||
# If the target id is not set, return
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
# If anyone comes home and the entity is not on, turn it on.
|
||||
if new_state.state == STATE_HOME and not core.is_on(hass, TARGET_ID):
|
||||
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
@track_time_change(hour=7, minute=0, second=0)
|
||||
def wake_up(hass, now):
|
||||
"""Turn light on in the morning.
|
||||
|
||||
Turn the light on at 7 AM if there are people home and it is not already
|
||||
on.
|
||||
"""
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
if device_tracker.is_on(hass) and not core.is_on(hass, TARGET_ID):
|
||||
_LOGGER.info('People home at 7AM, turning it on')
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
|
||||
@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 not TARGET_ID:
|
||||
return
|
||||
|
||||
if core.is_on(hass, TARGET_ID):
|
||||
_LOGGER.info('All lights have been turned off, turning it off')
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
|
||||
@service(DOMAIN, SERVICE_FLASH)
|
||||
def flash_service(hass, call):
|
||||
"""Service that will toggle the target.
|
||||
|
||||
Set the light to off for 10 seconds if on and vice versa.
|
||||
"""
|
||||
if not TARGET_ID:
|
||||
return
|
||||
|
||||
if core.is_on(hass, TARGET_ID):
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
else:
|
||||
core.turn_on(hass, TARGET_ID)
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
core.turn_off(hass, TARGET_ID)
|
||||
|
||||
@@ -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,17 @@ 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. """
|
||||
|
||||
# States are in the format DOMAIN.OBJECT_ID
|
||||
"""Setup our skeleton component."""
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Init file for Home Assistant."""
|
||||
|
||||
+128
-52
@@ -1,27 +1,37 @@
|
||||
""" Starts home assistant. """
|
||||
"""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 (
|
||||
__version__,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
REQUIRED_PYTHON_VER,
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
|
||||
|
||||
def validate_python():
|
||||
""" Validate we're running the right Python version. """
|
||||
"""Validate we're running the right Python version."""
|
||||
major, minor = sys.version_info[:2]
|
||||
req_major, req_minor = REQUIRED_PYTHON_VER
|
||||
|
||||
if major < 3 or (major == 3 and minor < 4):
|
||||
print("Home Assistant requires atleast Python 3.4")
|
||||
if major < req_major or (major == req_major and minor < req_minor):
|
||||
print("Home Assistant requires at least Python {}.{}".format(
|
||||
req_major, req_minor))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_config_path(config_dir):
|
||||
""" Validates configuration directory. """
|
||||
|
||||
"""Validate the configuration directory."""
|
||||
lib_dir = os.path.join(config_dir, 'lib')
|
||||
|
||||
# Test if configuration directory exists
|
||||
@@ -49,7 +59,7 @@ def ensure_config_path(config_dir):
|
||||
|
||||
|
||||
def ensure_config_file(config_dir):
|
||||
""" Ensure configuration file exists. """
|
||||
"""Ensure configuration file exists."""
|
||||
config_path = config_util.ensure_config_exists(config_dir)
|
||||
|
||||
if config_path is None:
|
||||
@@ -60,7 +70,7 @@ def ensure_config_file(config_dir):
|
||||
|
||||
|
||||
def get_arguments():
|
||||
""" Get parsed passed in arguments. """
|
||||
"""Get parsed passed in arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Home Assistant: Observe, Control, Automate.")
|
||||
parser.add_argument('--version', action='version', version=__version__)
|
||||
@@ -73,6 +83,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',
|
||||
@@ -120,25 +135,25 @@ def get_arguments():
|
||||
|
||||
|
||||
def daemonize():
|
||||
""" Move current process to daemon process """
|
||||
# create first fork
|
||||
"""Move current process to daemon process."""
|
||||
# Create first fork
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
|
||||
# decouple fork
|
||||
# Decouple fork
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
# create second fork
|
||||
# Create second fork
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def check_pid(pid_file):
|
||||
""" Check that HA is not already running """
|
||||
# check pid file
|
||||
"""Check that HA is not already running."""
|
||||
# Check pid file
|
||||
try:
|
||||
pid = int(open(pid_file, 'r').readline())
|
||||
except IOError:
|
||||
@@ -155,7 +170,7 @@ def check_pid(pid_file):
|
||||
|
||||
|
||||
def write_pid(pid_file):
|
||||
""" Create PID File """
|
||||
"""Create a PID File."""
|
||||
pid = os.getpid()
|
||||
try:
|
||||
open(pid_file, 'w').write(str(pid))
|
||||
@@ -165,7 +180,7 @@ def write_pid(pid_file):
|
||||
|
||||
|
||||
def install_osx():
|
||||
""" Setup to run via launchd on OS X """
|
||||
"""Setup to run via launchd on OS X."""
|
||||
with os.popen('which hass') as inp:
|
||||
hass_path = inp.read().strip()
|
||||
|
||||
@@ -197,42 +212,19 @@ def install_osx():
|
||||
|
||||
|
||||
def uninstall_osx():
|
||||
""" Unload from launchd on OS X """
|
||||
"""Unload from launchd on OS X."""
|
||||
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
|
||||
os.popen('launchctl unload ' + path)
|
||||
|
||||
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': {},
|
||||
@@ -251,7 +243,7 @@ def main():
|
||||
|
||||
if args.open_ui:
|
||||
def open_browser(event):
|
||||
""" Open the webinterface in a browser. """
|
||||
"""Open the webinterface in a browser."""
|
||||
if hass.config.api is not None:
|
||||
import webbrowser
|
||||
webbrowser.open(hass.config.api.base_url)
|
||||
@@ -259,7 +251,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):
|
||||
"""Run 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():
|
||||
"""Start 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())
|
||||
|
||||
+84
-73
@@ -1,36 +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
|
||||
from homeassistant.helpers.entity import Entity
|
||||
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 (
|
||||
__version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
|
||||
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_CUSTOMIZE,
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SETUP_LOCK = RLock()
|
||||
_CURRENT_SETUP = []
|
||||
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
@@ -39,8 +34,7 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
|
||||
|
||||
def setup_component(hass, domain, config=None):
|
||||
""" Setup a component and all its dependencies. """
|
||||
|
||||
"""Setup a component and all its dependencies."""
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
|
||||
@@ -63,7 +57,7 @@ def setup_component(hass, domain, config=None):
|
||||
|
||||
|
||||
def _handle_requirements(hass, component, name):
|
||||
""" Installs requirements for component. """
|
||||
"""Install the requirements for a component."""
|
||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||
return True
|
||||
|
||||
@@ -77,46 +71,61 @@ 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 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 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):
|
||||
""" Loads a platform and makes sure dependencies are setup. """
|
||||
"""Load a platform and makes sure dependencies are setup."""
|
||||
_ensure_loader_prepared(hass)
|
||||
|
||||
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
|
||||
@@ -133,14 +142,13 @@ def prepare_setup_platform(hass, config, domain, platform_name):
|
||||
return platform
|
||||
|
||||
# Load dependencies
|
||||
if hasattr(platform, 'DEPENDENCIES'):
|
||||
for component in platform.DEPENDENCIES:
|
||||
if not setup_component(hass, component, config):
|
||||
_LOGGER.error(
|
||||
'Unable to prepare setup for platform %s because '
|
||||
'dependency %s could not be initialized', platform_path,
|
||||
component)
|
||||
return None
|
||||
for component in getattr(platform, 'DEPENDENCIES', []):
|
||||
if not setup_component(hass, component, config):
|
||||
_LOGGER.error(
|
||||
'Unable to prepare setup for platform %s because '
|
||||
'dependency %s could not be initialized', platform_path,
|
||||
component)
|
||||
return None
|
||||
|
||||
if not _handle_requirements(hass, platform, platform_path):
|
||||
return None
|
||||
@@ -149,7 +157,7 @@ def prepare_setup_platform(hass, config, domain, platform_name):
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir):
|
||||
""" Add local library to Python Path """
|
||||
"""Add local library to Python Path."""
|
||||
sys.path.insert(0, os.path.join(config_dir, 'lib'))
|
||||
|
||||
|
||||
@@ -157,8 +165,7 @@ def mount_local_lib_path(config_dir):
|
||||
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
verbose=False, daemon=False, skip_pip=False,
|
||||
log_rotate_days=None):
|
||||
"""
|
||||
Tries to configure Home Assistant from a config dict.
|
||||
"""Try to configure Home Assistant from a config dict.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
"""
|
||||
@@ -200,6 +207,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
|
||||
# Setup the components
|
||||
for domain in loader.load_order_components(components):
|
||||
_setup_component(hass, domain, config)
|
||||
@@ -209,9 +220,9 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
|
||||
def from_config_file(config_path, hass=None, verbose=False, daemon=False,
|
||||
skip_pip=True, log_rotate_days=None):
|
||||
"""
|
||||
Reads the configuration file and tries to start all the required
|
||||
functionality. Will add functionality to 'hass' parameter if given,
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
instantiates a new Home Assistant object if 'hass' is not given.
|
||||
"""
|
||||
if hass is None:
|
||||
@@ -224,14 +235,14 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False,
|
||||
|
||||
enable_logging(hass, verbose, daemon, log_rotate_days)
|
||||
|
||||
config_dict = config_util.load_config_file(config_path)
|
||||
config_dict = config_util.load_yaml_config_file(config_path)
|
||||
|
||||
return from_config_dict(config_dict, hass, enable_log=False,
|
||||
skip_pip=skip_pip)
|
||||
|
||||
|
||||
def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
|
||||
""" Setup the logging for home assistant. """
|
||||
"""Setup the logging."""
|
||||
if not daemon:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
@@ -276,7 +287,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
|
||||
datefmt='%y-%m-%d %H:%M:%S'))
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(err_handler)
|
||||
logger.setLevel(logging.INFO) # this sets the minimum log level
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
else:
|
||||
_LOGGER.error(
|
||||
@@ -284,7 +295,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
|
||||
|
||||
|
||||
def process_ha_config_upgrade(hass):
|
||||
""" Upgrade config if necessary. """
|
||||
"""Upgrade config if necessary."""
|
||||
version_path = hass.config.path('.HA_VERSION')
|
||||
|
||||
try:
|
||||
@@ -309,11 +320,11 @@ def process_ha_config_upgrade(hass):
|
||||
|
||||
|
||||
def process_ha_core_config(hass, config):
|
||||
""" Processes the [homeassistant] section from the config. """
|
||||
"""Process the [homeassistant] section from the config."""
|
||||
hac = hass.config
|
||||
|
||||
def set_time_zone(time_zone_str):
|
||||
""" Helper method to set time zone in HA. """
|
||||
"""Helper method to set time zone."""
|
||||
if time_zone_str is None:
|
||||
return
|
||||
|
||||
@@ -384,6 +395,6 @@ def process_ha_core_config(hass, config):
|
||||
|
||||
|
||||
def _ensure_loader_prepared(hass):
|
||||
""" Ensure Home Assistant loader is prepared. """
|
||||
"""Ensure Home Assistant loader is prepared."""
|
||||
if not loader.PREPARED:
|
||||
loader.prepare(hass)
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
"""
|
||||
homeassistant.components
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
This package contains components that can be plugged into Home Assistant.
|
||||
|
||||
Component design guidelines:
|
||||
|
||||
Each component defines a constant DOMAIN that is equal to its filename.
|
||||
|
||||
Each component that tracks states should create state entity names in the
|
||||
format "<DOMAIN>.<OBJECT_ID>".
|
||||
|
||||
Each component should publish services only under its own domain.
|
||||
- Each component defines a constant DOMAIN that is equal to its filename.
|
||||
- Each component that tracks states should create state entity names in the
|
||||
format "<DOMAIN>.<OBJECT_ID>".
|
||||
- Each component should publish services only under its own domain.
|
||||
"""
|
||||
import itertools as it
|
||||
import logging
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers import extract_entity_ids
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Loads up the module to call the is_on method.
|
||||
If there is no entity id given we will check all. """
|
||||
"""Load up the module to call the is_on method.
|
||||
|
||||
If there is no entity id given we will check all.
|
||||
"""
|
||||
if entity_id:
|
||||
group = get_component('group')
|
||||
|
||||
@@ -36,7 +33,7 @@ def is_on(hass, entity_id=None):
|
||||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = util.split_entity_id(entity_id)[0]
|
||||
domain = split_entity_id(entity_id)[0]
|
||||
|
||||
module = get_component(domain)
|
||||
|
||||
@@ -53,7 +50,7 @@ def is_on(hass, entity_id=None):
|
||||
|
||||
|
||||
def turn_on(hass, entity_id=None, **service_data):
|
||||
""" Turns specified entity on if possible. """
|
||||
"""Turn specified entity on if possible."""
|
||||
if entity_id is not None:
|
||||
service_data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
@@ -61,18 +58,25 @@ def turn_on(hass, entity_id=None, **service_data):
|
||||
|
||||
|
||||
def turn_off(hass, entity_id=None, **service_data):
|
||||
""" Turns specified entity off. """
|
||||
"""Turn specified entity off."""
|
||||
if entity_id is not None:
|
||||
service_data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup general services related to homeassistant. """
|
||||
def toggle(hass, entity_id=None, **service_data):
|
||||
"""Toggle specified entity."""
|
||||
if entity_id is not None:
|
||||
service_data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup general services related to Home Assistant."""
|
||||
def handle_turn_service(service):
|
||||
""" Method to handle calls to homeassistant.turn_on/off. """
|
||||
"""Method to handle calls to homeassistant.turn_on/off."""
|
||||
entity_ids = extract_entity_ids(hass, service)
|
||||
|
||||
# Generic turn on/off method requires entity id
|
||||
@@ -84,18 +88,27 @@ def setup(hass, config):
|
||||
|
||||
# Group entity_ids by domain. groupby requires sorted data.
|
||||
by_domain = it.groupby(sorted(entity_ids),
|
||||
lambda item: util.split_entity_id(item)[0])
|
||||
lambda item: split_entity_id(item)[0])
|
||||
|
||||
for domain, ent_ids in by_domain:
|
||||
# We want to block for all calls and only return when all calls
|
||||
# have been processed. If a service does not exist it causes a 10
|
||||
# second delay while we're blocking waiting for a response.
|
||||
# But services can be registered on other HA instances that are
|
||||
# listening to the bus too. So as a in between solution, we'll
|
||||
# block only if the service is defined in the current HA instance.
|
||||
blocking = hass.services.has_service(domain, service.service)
|
||||
|
||||
# Create a new dict for this call
|
||||
data = dict(service.data)
|
||||
|
||||
# ent_ids is a generator, convert it to a list.
|
||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||
|
||||
hass.services.call(domain, service.service, data, True)
|
||||
hass.services.call(domain, service.service, data, blocking)
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with a alarm control panel.
|
||||
Component to interface with an alarm control panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components import verisure
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
verisure.DISCOVER_SENSORS: 'verisure'
|
||||
verisure.DISCOVER_ALARMS: 'verisure'
|
||||
}
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
@@ -32,9 +32,6 @@ SERVICE_TO_METHOD = {
|
||||
SERVICE_ALARM_TRIGGER: 'alarm_trigger'
|
||||
}
|
||||
|
||||
ATTR_CODE = 'code'
|
||||
ATTR_CODE_FORMAT = 'code_format'
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
ATTR_CODE,
|
||||
ATTR_CODE_FORMAT
|
||||
@@ -42,7 +39,7 @@ ATTR_TO_PROPERTY = [
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for sensors. """
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
@@ -50,7 +47,7 @@ def setup(hass, config):
|
||||
component.setup(config)
|
||||
|
||||
def alarm_service_handler(service):
|
||||
""" Maps services to methods on Alarm. """
|
||||
"""Map services to methods on Alarm."""
|
||||
target_alarms = component.extract_from_service(service)
|
||||
|
||||
if ATTR_CODE not in service.data:
|
||||
@@ -62,6 +59,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'))
|
||||
@@ -74,7 +73,7 @@ def setup(hass, config):
|
||||
|
||||
|
||||
def alarm_disarm(hass, code=None, entity_id=None):
|
||||
""" Send the alarm the command for disarm. """
|
||||
"""Send the alarm the command for disarm."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
@@ -85,7 +84,7 @@ def alarm_disarm(hass, code=None, entity_id=None):
|
||||
|
||||
|
||||
def alarm_arm_home(hass, code=None, entity_id=None):
|
||||
""" Send the alarm the command for arm home. """
|
||||
"""Send the alarm the command for arm home."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
@@ -96,7 +95,7 @@ def alarm_arm_home(hass, code=None, entity_id=None):
|
||||
|
||||
|
||||
def alarm_arm_away(hass, code=None, entity_id=None):
|
||||
""" Send the alarm the command for arm away. """
|
||||
"""Send the alarm the command for arm away."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
@@ -107,7 +106,7 @@ def alarm_arm_away(hass, code=None, entity_id=None):
|
||||
|
||||
|
||||
def alarm_trigger(hass, code=None, entity_id=None):
|
||||
""" Send the alarm the command for trigger. """
|
||||
"""Send the alarm the command for trigger."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
@@ -119,33 +118,33 @@ def alarm_trigger(hass, code=None, entity_id=None):
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
""" ABC for alarm control devices. """
|
||||
"""An abstract class for alarm control devices."""
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" regex for code format or None if no code is required. """
|
||||
"""Regex for code format or None if no code is required."""
|
||||
return None
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
"""Send disarm command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
"""Send arm home command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
"""Send arm away command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
""" Send alarm trigger command. """
|
||||
"""Send alarm trigger command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
"""Return the state attributes."""
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
}
|
||||
}
|
||||
return state_attr
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Interfaces with Alarm.com alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
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.1.1.zip'
|
||||
'#pyalarmdotcom==0.1.1']
|
||||
DEFAULT_NAME = 'Alarm.com'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup an Alarm.com control panel."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error('Must specify username and password!')
|
||||
return False
|
||||
|
||||
add_devices([AlarmDotCom(hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('code'),
|
||||
username,
|
||||
password)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
"""Represent an Alarm.com status."""
|
||||
|
||||
def __init__(self, hass, name, code, username, password):
|
||||
"""Initialize the Alarm.com status."""
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
self._alarm = Alarmdotcom(username, password, timeout=10)
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alarm."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._alarm.state == 'Disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
elif self._alarm.state == 'Armed Stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state == 'Armed Away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.disarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_stay()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_away()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered for %s', state)
|
||||
return check
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
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):
|
||||
"""Setup the Demo alarm control panel platform."""
|
||||
add_devices([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10),
|
||||
])
|
||||
@@ -1,33 +1,28 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel.manual
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
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__)
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the manual alarm platform. """
|
||||
|
||||
"""Setup the manual alarm platform."""
|
||||
add_devices([ManualAlarm(
|
||||
hass,
|
||||
config.get('name', DEFAULT_ALARM_NAME),
|
||||
@@ -49,6 +44,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time, trigger_time):
|
||||
"""Initalize the manual alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
@@ -59,18 +55,19 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self._state in (STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) and \
|
||||
"""Return the state of the device."""
|
||||
if self._state in (STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY) and \
|
||||
self._pending_time and self._state_ts + self._pending_time > \
|
||||
dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
@@ -86,11 +83,11 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" One or more characters. """
|
||||
"""One or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, STATE_ALARM_DISARMED):
|
||||
return
|
||||
|
||||
@@ -99,7 +96,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self.update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
|
||||
return
|
||||
|
||||
@@ -113,7 +110,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
|
||||
return
|
||||
|
||||
@@ -127,7 +124,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
""" Send alarm trigger command. No code needed. """
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
@@ -142,7 +139,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._state_ts + self._pending_time + self._trigger_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
""" Validate given code. """
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Invalid code given for %s', state)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
This platform enables the possibility to control a MQTT alarm.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.mqtt/
|
||||
"""
|
||||
import 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__)
|
||||
@@ -26,8 +24,7 @@ DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the MQTT platform. """
|
||||
|
||||
"""Setup the MQTT platform."""
|
||||
if config.get('state_topic') is None:
|
||||
_LOGGER.error("Missing required variable: state_topic")
|
||||
return False
|
||||
@@ -51,10 +48,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class MqttAlarm(alarm.AlarmControlPanel):
|
||||
""" represents a MQTT alarm status within home assistant. """
|
||||
"""Represent a MQTT alarm status."""
|
||||
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code):
|
||||
"""Initalize the MQTT alarm panel."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
@@ -67,7 +65,7 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
self._code = str(code) if code else None
|
||||
|
||||
def message_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
"""A new MQTT message has been received."""
|
||||
if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED):
|
||||
@@ -80,47 +78,47 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed """
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" One or more characters if code is defined """
|
||||
"""One or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_disarm, self._qos)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_home, self._qos)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
return
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_away, self._qos)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
""" Validate given code. """
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered for %s', state)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
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 platform."""
|
||||
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):
|
||||
"""Represents the NX584-based alarm panel."""
|
||||
|
||||
def __init__(self, hass, host, name):
|
||||
"""Initalize the nx584 alarm panel."""
|
||||
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):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return 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()
|
||||
@@ -1,97 +1,90 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel.verisure
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Interfaces with Verisure alarm control panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/verisure/
|
||||
https://home-assistant.io/components/alarm_control_panel.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__)
|
||||
|
||||
|
||||
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
|
||||
|
||||
"""Setup the Verisure platform."""
|
||||
alarms = []
|
||||
|
||||
alarms.extend([
|
||||
VerisureAlarm(value)
|
||||
for value in verisure.get_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)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
""" Represents a Verisure alarm status. """
|
||||
"""Represent a Verisure alarm status."""
|
||||
|
||||
def __init__(self, alarm_status):
|
||||
self._id = alarm_status.id
|
||||
self._device = verisure.MY_PAGES.DEVICE_ALARM
|
||||
def __init__(self, device_id):
|
||||
"""Initalize the Verisure alarm panel."""
|
||||
self._id = device_id
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = int(hub.config.get('code_digits', '4'))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
"""Return the name of the device."""
|
||||
return 'Alarm {}'.format(self._id)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" Four digit code required. """
|
||||
return '^\\d{4}$'
|
||||
"""The code format as regex."""
|
||||
return '^\\d{%s}$' % self._digits
|
||||
|
||||
def update(self):
|
||||
""" Update alarm status """
|
||||
verisure.update()
|
||||
"""Update alarm status."""
|
||||
hub.update_alarms()
|
||||
|
||||
if verisure.STATUS[self._device][self._id].status == 'unarmed':
|
||||
if hub.alarm_status[self._id].status == 'unarmed':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedhome':
|
||||
elif hub.alarm_status[self._id].status == 'armedhome':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedaway':
|
||||
elif hub.alarm_status[self._id].status == 'armed':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif verisure.STATUS[self._device][self._id].status != 'pending':
|
||||
elif hub.alarm_status[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
'Unknown alarm state %s',
|
||||
verisure.STATUS[self._device][self._id].status)
|
||||
hub.alarm_status[self._id].status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_DISARMED)
|
||||
_LOGGER.warning('disarming')
|
||||
"""Send disarm command."""
|
||||
hub.my_pages.alarm.set(code, 'DISARMED')
|
||||
_LOGGER.info('verisure alarm disarming')
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_ARMED_HOME)
|
||||
_LOGGER.warning('arming home')
|
||||
"""Send arm home command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_HOME')
|
||||
_LOGGER.info('verisure alarm arming home')
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_ARMED_AWAY)
|
||||
_LOGGER.warning('arming away')
|
||||
"""Send arm away command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
|
||||
_LOGGER.info('verisure alarm arming away')
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
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/
|
||||
"""
|
||||
import enum
|
||||
import logging
|
||||
|
||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
from homeassistant.helpers import template
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_CONFIG = {}
|
||||
|
||||
API_ENDPOINT = '/api/alexa'
|
||||
|
||||
CONF_INTENTS = 'intents'
|
||||
CONF_CARD = 'card'
|
||||
CONF_SPEECH = 'speech'
|
||||
CONF_ACTION = 'action'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
_CONFIG.update(config[DOMAIN].get(CONF_INTENTS, {}))
|
||||
|
||||
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_alexa(handler, path_match, data):
|
||||
"""Handle Alexa."""
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
|
||||
req = data.get('request')
|
||||
|
||||
if req is None:
|
||||
_LOGGER.error('Received invalid data from Alexa: %s', data)
|
||||
handler.write_json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
req_type = req['type']
|
||||
|
||||
if req_type == 'SessionEndedRequest':
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(handler.server.hass, intent)
|
||||
|
||||
if req_type == 'LaunchRequest':
|
||||
response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"Hello, and welcome to the future. How may I help?")
|
||||
handler.write_json(response.as_dict())
|
||||
return
|
||||
|
||||
if req_type != 'IntentRequest':
|
||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
||||
return
|
||||
|
||||
intent_name = intent['name']
|
||||
config = _CONFIG.get(intent_name)
|
||||
|
||||
if config is None:
|
||||
_LOGGER.warning('Received unknown intent %s', intent_name)
|
||||
response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"This intent is not yet configured within Home Assistant.")
|
||||
handler.write_json(response.as_dict())
|
||||
return
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(SpeechType[speech['type']], speech['text'])
|
||||
|
||||
if card is not None:
|
||||
response.add_card(CardType[card['type']], card['title'],
|
||||
card['content'])
|
||||
|
||||
if action is not None:
|
||||
call_from_config(handler.server.hass, action, True)
|
||||
|
||||
handler.write_json(response.as_dict())
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
"""The Alexa speech types."""
|
||||
|
||||
plaintext = "PlainText"
|
||||
ssml = "SSML"
|
||||
|
||||
|
||||
class CardType(enum.Enum):
|
||||
"""The Alexa card types."""
|
||||
|
||||
simple = "Simple"
|
||||
link_account = "LinkAccount"
|
||||
|
||||
|
||||
class AlexaResponse(object):
|
||||
"""Help generating the response for Alexa."""
|
||||
|
||||
def __init__(self, hass, intent=None):
|
||||
"""Initialize the response."""
|
||||
self.hass = hass
|
||||
self.speech = None
|
||||
self.card = None
|
||||
self.reprompt = None
|
||||
self.session_attributes = {}
|
||||
self.should_end_session = True
|
||||
if intent is not None and 'slots' in intent:
|
||||
self.variables = {key: value['value'] for key, value
|
||||
in intent['slots'].items() if 'value' in value}
|
||||
else:
|
||||
self.variables = {}
|
||||
|
||||
def add_card(self, card_type, title, content):
|
||||
"""Add a card to the response."""
|
||||
assert self.card is None
|
||||
|
||||
card = {
|
||||
"type": card_type.value
|
||||
}
|
||||
|
||||
if card_type == CardType.link_account:
|
||||
self.card = card
|
||||
return
|
||||
|
||||
card["title"] = self._render(title),
|
||||
card["content"] = self._render(content)
|
||||
self.card = card
|
||||
|
||||
def add_speech(self, speech_type, text):
|
||||
"""Add speech to the response."""
|
||||
assert self.speech is None
|
||||
|
||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||
|
||||
self.speech = {
|
||||
'type': speech_type.value,
|
||||
key: self._render(text)
|
||||
}
|
||||
|
||||
def add_reprompt(self, speech_type, text):
|
||||
"""Add reprompt if user does not answer."""
|
||||
assert self.reprompt is None
|
||||
|
||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||
|
||||
self.reprompt = {
|
||||
'type': speech_type.value,
|
||||
key: self._render(text)
|
||||
}
|
||||
|
||||
def as_dict(self):
|
||||
"""Return response in an Alexa valid dict."""
|
||||
response = {
|
||||
'shouldEndSession': self.should_end_session
|
||||
}
|
||||
|
||||
if self.card is not None:
|
||||
response['card'] = self.card
|
||||
|
||||
if self.speech is not None:
|
||||
response['outputSpeech'] = self.speech
|
||||
|
||||
if self.reprompt is not None:
|
||||
response['reprompt'] = {
|
||||
'outputSpeech': self.reprompt
|
||||
}
|
||||
|
||||
return {
|
||||
'version': '1.0',
|
||||
'sessionAttributes': self.session_attributes,
|
||||
'response': response,
|
||||
}
|
||||
|
||||
def _render(self, template_string):
|
||||
"""Render a response, adding data from intent if available."""
|
||||
return template.render(self.hass, template_string, self.variables)
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Support for 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):
|
||||
"""Initialize the data oject."""
|
||||
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."""
|
||||
self._status = self._get_status()
|
||||
+105
-73
@@ -1,28 +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.helpers.state import TrackStates
|
||||
import homeassistant.remote as rem
|
||||
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,
|
||||
EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
|
||||
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
HTTP_UNPROCESSABLE_ENTITY, 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']
|
||||
@@ -34,12 +33,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Register the API with the HTTP interface. """
|
||||
|
||||
if 'http' not in hass.config.components:
|
||||
_LOGGER.error('Dependency http is not loaded')
|
||||
return False
|
||||
|
||||
"""Register the API with the HTTP interface."""
|
||||
# /api - for validation purposes
|
||||
hass.http.register_path('GET', URL_API, _handle_get_api)
|
||||
|
||||
@@ -49,10 +43,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(
|
||||
@@ -64,6 +54,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)
|
||||
@@ -93,56 +86,69 @@ def setup(hass, config):
|
||||
hass.http.register_path('GET', URL_API_ERROR_LOG,
|
||||
_handle_get_api_error_log)
|
||||
|
||||
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
|
||||
|
||||
hass.http.register_path('POST', URL_API_TEMPLATE,
|
||||
_handle_post_api_template)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api(handler, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
"""Render 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
|
||||
write_lock = threading.Lock()
|
||||
block = threading.Event()
|
||||
session_id = None
|
||||
|
||||
restrict = data.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',')
|
||||
|
||||
def write_message(payload):
|
||||
""" Writes a message to the output. """
|
||||
"""Write a message to the output."""
|
||||
with write_lock:
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
|
||||
try:
|
||||
wfile.write(msg.encode("UTF-8"))
|
||||
wfile.flush()
|
||||
except IOError:
|
||||
except (IOError, ValueError):
|
||||
# IOError: socket errors
|
||||
# ValueError: raised when 'I/O operation on closed file'
|
||||
block.set()
|
||||
|
||||
def forward_events(event):
|
||||
""" Forwards events to the open request. """
|
||||
"""Forward events to the open request."""
|
||||
nonlocal gracefully_closed
|
||||
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED or \
|
||||
restrict and event.event_type not in restrict:
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
gracefully_closed = True
|
||||
block.set()
|
||||
return
|
||||
|
||||
handler.server.sessions.extend_validation(session_id)
|
||||
write_message(json.dumps(event, cls=rem.JSONEncoder))
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header('Content-type', 'text/event-stream')
|
||||
session_id = handler.set_session_cookie_header()
|
||||
handler.end_headers()
|
||||
|
||||
hass.bus.listen(MATCH_ALL, forward_events)
|
||||
if restrict:
|
||||
for event in restrict:
|
||||
hass.bus.listen(event, forward_events)
|
||||
else:
|
||||
hass.bus.listen(MATCH_ALL, forward_events)
|
||||
|
||||
while True:
|
||||
write_message(STREAM_PING_PAYLOAD)
|
||||
@@ -156,33 +162,25 @@ def _handle_get_api_stream(handler, path_match, data):
|
||||
_LOGGER.info("Found broken event stream to %s, cleaning up",
|
||||
handler.client_address[0])
|
||||
|
||||
hass.bus.remove_listener(MATCH_ALL, forward_events)
|
||||
if restrict:
|
||||
for event in restrict:
|
||||
hass.bus.remove_listener(event, forward_events)
|
||||
else:
|
||||
hass.bus.remove_listener(MATCH_ALL, forward_events)
|
||||
|
||||
|
||||
def _handle_get_api_config(handler, path_match, data):
|
||||
""" Returns the Home Assistant config. """
|
||||
"""Return 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. """
|
||||
"""Return 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. """
|
||||
"""Return the state of a specific entity."""
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
state = handler.server.hass.states.get(entity_id)
|
||||
@@ -194,7 +192,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.
|
||||
"""Handle updating the state of an entity.
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
@@ -224,16 +222,31 @@ 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))
|
||||
"""Handle 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.
|
||||
"""Handle firing of an event.
|
||||
|
||||
This handles the following paths:
|
||||
/api/events/<event_type>
|
||||
This handles the following paths: /api/events/<event_type>
|
||||
|
||||
Events from /api are threated as remote events.
|
||||
"""
|
||||
@@ -242,6 +255,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
|
||||
|
||||
@@ -260,16 +274,15 @@ 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))
|
||||
"""Handle 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.
|
||||
"""Handle calling a service.
|
||||
|
||||
This handles the following paths:
|
||||
/api/services/<domain>/<service>
|
||||
This handles the following paths: /api/services/<domain>/<service>
|
||||
"""
|
||||
domain = path_match.group('domain')
|
||||
service = path_match.group('service')
|
||||
@@ -282,8 +295,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. """
|
||||
|
||||
"""Handle adding an event forwarding target."""
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
@@ -316,8 +328,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. """
|
||||
|
||||
"""Handle deleting an event forwarding target."""
|
||||
try:
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
@@ -340,25 +351,46 @@ def _handle_delete_api_event_forward(handler, path_match, data):
|
||||
|
||||
|
||||
def _handle_get_api_components(handler, path_match, data):
|
||||
""" Returns all the loaded components. """
|
||||
|
||||
"""Return 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. """
|
||||
error_path = handler.server.hass.config.path(ERROR_LOG_FILENAME)
|
||||
with open(error_path, 'rb') as error_log:
|
||||
handler.write_file_pointer(CONTENT_TYPE_TEXT_PLAIN, error_log)
|
||||
"""Return the logged errors for this session."""
|
||||
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
|
||||
False)
|
||||
|
||||
|
||||
def _services_json(hass):
|
||||
""" Generate services data to JSONify. """
|
||||
def _handle_post_api_log_out(handler, path_match, data):
|
||||
"""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."""
|
||||
template_string = data.get('template', '')
|
||||
|
||||
try:
|
||||
rendered = template.render(handler.server.hass, template_string)
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
|
||||
handler.end_headers()
|
||||
handler.wfile.write(rendered.encode('utf-8'))
|
||||
except TemplateError as e:
|
||||
handler.write_json_message(str(e), HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
|
||||
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()]
|
||||
|
||||
@@ -1,45 +1,29 @@
|
||||
"""
|
||||
components.arduino
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
Arduino component that connects to a directly attached Arduino board which
|
||||
runs with the Firmata firmware.
|
||||
Support for Arduino boards running with the Firmata firmware.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/arduino/
|
||||
"""
|
||||
import logging
|
||||
|
||||
try:
|
||||
from PyMata.pymata import PyMata
|
||||
except ImportError:
|
||||
PyMata = None
|
||||
|
||||
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"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['PyMata==2.07a']
|
||||
BOARD = None
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup the Arduino component. """
|
||||
|
||||
global PyMata # pylint: disable=invalid-name
|
||||
if PyMata is None:
|
||||
from PyMata.pymata import PyMata as PyMata_
|
||||
PyMata = PyMata_
|
||||
|
||||
import serial
|
||||
|
||||
"""Setup the Arduino component."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: ['port']},
|
||||
_LOGGER):
|
||||
return False
|
||||
|
||||
import serial
|
||||
global BOARD
|
||||
try:
|
||||
BOARD = ArduinoBoard(config[DOMAIN]['port'])
|
||||
@@ -52,11 +36,11 @@ def setup(hass, config):
|
||||
return False
|
||||
|
||||
def stop_arduino(event):
|
||||
""" Stop the Arduino service. """
|
||||
"""Stop the Arduino service."""
|
||||
BOARD.disconnect()
|
||||
|
||||
def start_arduino(event):
|
||||
""" Start the Arduino service. """
|
||||
"""Start the Arduino service."""
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_arduino)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_arduino)
|
||||
@@ -65,14 +49,16 @@ def setup(hass, config):
|
||||
|
||||
|
||||
class ArduinoBoard(object):
|
||||
""" Represents an Arduino board. """
|
||||
"""Representation of an Arduino board."""
|
||||
|
||||
def __init__(self, port):
|
||||
"""Initialize the board."""
|
||||
from PyMata.pymata import PyMata
|
||||
self._port = port
|
||||
self._board = PyMata(self._port, verbose=False)
|
||||
|
||||
def set_mode(self, pin, direction, mode):
|
||||
""" Sets the mode and the direction of a given pin. """
|
||||
"""Set the mode and the direction of a given pin."""
|
||||
if mode == 'analog' and direction == 'in':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.INPUT,
|
||||
@@ -95,31 +81,31 @@ class ArduinoBoard(object):
|
||||
self._board.PWM)
|
||||
|
||||
def get_analog_inputs(self):
|
||||
""" Get the values from the pins. """
|
||||
"""Get the values from the pins."""
|
||||
self._board.capability_query()
|
||||
return self._board.get_analog_response_table()
|
||||
|
||||
def set_digital_out_high(self, pin):
|
||||
""" Sets a given digital pin to high. """
|
||||
"""Set a given digital pin to high."""
|
||||
self._board.digital_write(pin, 1)
|
||||
|
||||
def set_digital_out_low(self, pin):
|
||||
""" Sets a given digital pin to low. """
|
||||
"""Set a given digital pin to low."""
|
||||
self._board.digital_write(pin, 0)
|
||||
|
||||
def get_digital_in(self, pin):
|
||||
""" Gets the value from a given digital pin. """
|
||||
"""Get the value from a given digital pin."""
|
||||
self._board.digital_read(pin)
|
||||
|
||||
def get_analog_in(self, pin):
|
||||
""" Gets the value from a given analog pin. """
|
||||
"""Get the value from a given analog pin."""
|
||||
self._board.analog_read(pin)
|
||||
|
||||
def get_firmata(self):
|
||||
""" Return the version of the Firmata firmware. """
|
||||
"""Return the version of the Firmata firmware."""
|
||||
return self._board.get_firmata_version()
|
||||
|
||||
def disconnect(self):
|
||||
""" Disconnects the board and closes the serial connection. """
|
||||
"""Disconnect the board and close the serial connection."""
|
||||
self._board.reset()
|
||||
self._board.close()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
homeassistant.components.automation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to setup simple automation rules via the config file.
|
||||
Allow to setup simple automation rules via the config file.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/automation/
|
||||
@@ -9,18 +7,17 @@ https://home-assistant.io/components/automation/
|
||||
import logging
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.util import split_entity_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
from homeassistant.helpers.service import validate_service_call
|
||||
|
||||
|
||||
DOMAIN = 'automation'
|
||||
|
||||
DEPENDENCIES = ['group']
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
CONF_SERVICE = 'service'
|
||||
CONF_SERVICE_ENTITY_ID = 'entity_id'
|
||||
CONF_SERVICE_DATA = 'data'
|
||||
|
||||
CONF_CONDITION = 'condition'
|
||||
CONF_ACTION = 'action'
|
||||
@@ -37,25 +34,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up automation. """
|
||||
"""Setup the automation."""
|
||||
config_key = DOMAIN
|
||||
found = 1
|
||||
|
||||
while config_key in config:
|
||||
# check for one block syntax
|
||||
# Check for one block syntax
|
||||
if isinstance(config[config_key], dict):
|
||||
config_block = _migrate_old_config(config[config_key])
|
||||
name = config_block.get(CONF_ALIAS, config_key)
|
||||
_setup_automation(hass, config_block, name, config)
|
||||
|
||||
# check for multiple block syntax
|
||||
# Check for multiple block syntax
|
||||
elif isinstance(config[config_key], list):
|
||||
for list_no, config_block in enumerate(config[config_key]):
|
||||
name = config_block.get(CONF_ALIAS,
|
||||
"{}, {}".format(config_key, list_no))
|
||||
_setup_automation(hass, config_block, name, config)
|
||||
|
||||
# any scalar value is incorrect
|
||||
# Any scalar value is incorrect
|
||||
else:
|
||||
_LOGGER.error('Error in config in section %s.', config_key)
|
||||
|
||||
@@ -66,8 +63,7 @@ def setup(hass, config):
|
||||
|
||||
|
||||
def _setup_automation(hass, config_block, name, config):
|
||||
""" Setup one instance of automation """
|
||||
|
||||
"""Setup one instance of automation."""
|
||||
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)
|
||||
|
||||
if action is None:
|
||||
@@ -85,45 +81,30 @@ def _setup_automation(hass, config_block, name, config):
|
||||
|
||||
|
||||
def _get_action(hass, config, name):
|
||||
""" Return an action based on a config. """
|
||||
|
||||
if CONF_SERVICE not in config:
|
||||
_LOGGER.error('Error setting up %s, no action specified.', name)
|
||||
"""Return an action based on a configuration."""
|
||||
validation_error = validate_service_call(config)
|
||||
if validation_error:
|
||||
_LOGGER.error(validation_error)
|
||||
return None
|
||||
|
||||
def action():
|
||||
""" Action to be executed. """
|
||||
"""Action to be executed."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
service_data = config.get(CONF_SERVICE_DATA, {})
|
||||
|
||||
if not isinstance(service_data, dict):
|
||||
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
|
||||
service_data = {}
|
||||
|
||||
if CONF_SERVICE_ENTITY_ID in config:
|
||||
try:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID].split(",")
|
||||
except AttributeError:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID]
|
||||
|
||||
hass.services.call(domain, service, service_data)
|
||||
call_from_config(hass, config)
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _migrate_old_config(config):
|
||||
""" Migrate old config to new. """
|
||||
"""Migrate old configuration to new."""
|
||||
if CONF_PLATFORM not in config:
|
||||
return config
|
||||
|
||||
_LOGGER.warning(
|
||||
'You are using an old configuration format. Please upgrade: '
|
||||
'https://home-assistant.io/components/automation.html')
|
||||
'https://home-assistant.io/components/automation/')
|
||||
|
||||
new_conf = {
|
||||
CONF_TRIGGER: dict(config),
|
||||
@@ -151,8 +132,7 @@ def _migrate_old_config(config):
|
||||
|
||||
|
||||
def _process_if(hass, config, p_config, action):
|
||||
""" Processes if checks. """
|
||||
|
||||
"""Process if checks."""
|
||||
cond_type = p_config.get(CONF_CONDITION_TYPE,
|
||||
DEFAULT_CONDITION_TYPE).lower()
|
||||
|
||||
@@ -182,12 +162,12 @@ def _process_if(hass, config, p_config, action):
|
||||
|
||||
if cond_type == CONDITION_TYPE_AND:
|
||||
def if_action():
|
||||
""" AND all conditions. """
|
||||
"""AND all conditions."""
|
||||
if all(check() for check in checks):
|
||||
action()
|
||||
else:
|
||||
def if_action():
|
||||
""" OR all conditions. """
|
||||
"""OR all conditions."""
|
||||
if any(check() for check in checks):
|
||||
action()
|
||||
|
||||
@@ -195,7 +175,7 @@ def _process_if(hass, config, p_config, action):
|
||||
|
||||
|
||||
def _process_trigger(hass, config, trigger_configs, name, action):
|
||||
""" Setup triggers. """
|
||||
"""Setup the triggers."""
|
||||
if isinstance(trigger_configs, dict):
|
||||
trigger_configs = [trigger_configs]
|
||||
|
||||
@@ -212,7 +192,7 @@ def _process_trigger(hass, config, trigger_configs, name, action):
|
||||
|
||||
|
||||
def _resolve_platform(method, hass, config, platform):
|
||||
""" Find automation platform. """
|
||||
"""Find the automation platform."""
|
||||
if platform is None:
|
||||
return None
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
homeassistant.components.automation.event
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers event listening automation rules.
|
||||
Offer event listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#event-trigger
|
||||
@@ -15,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for events based on config. """
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
|
||||
if event_type is None:
|
||||
@@ -25,7 +23,7 @@ def trigger(hass, config, action):
|
||||
event_data = config.get(CONF_EVENT_DATA)
|
||||
|
||||
def handle_event(event):
|
||||
""" Listens for events and calls the action when data matches. """
|
||||
"""Listen for events and calls the action when data matches."""
|
||||
if not event_data or all(val == event.data.get(key) for key, val
|
||||
in event_data.items()):
|
||||
action()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
homeassistant.components.automation.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers MQTT listening automation rules.
|
||||
Offer MQTT listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||
@@ -17,7 +15,7 @@ CONF_PAYLOAD = 'payload'
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
"""Listen for state changes based on configuration."""
|
||||
topic = config.get(CONF_TOPIC)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
|
||||
@@ -27,7 +25,7 @@ def trigger(hass, config, action):
|
||||
return False
|
||||
|
||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||
""" Listens for MQTT messages. """
|
||||
"""Listen for MQTT messages."""
|
||||
if payload is None or payload == msg_payload:
|
||||
action()
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"""
|
||||
homeassistant.components.automation.numeric_state
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers numeric state listening automation rules.
|
||||
Offer numeric state listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
|
||||
from homeassistant.helpers import template
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_BELOW = "below"
|
||||
@@ -18,8 +18,16 @@ CONF_ABOVE = "above"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _renderer(hass, value_template, state):
|
||||
"""Render the state value."""
|
||||
if value_template is None:
|
||||
return state.state
|
||||
|
||||
return template.render(hass, value_template, {'state': state})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
@@ -28,6 +36,7 @@ def trigger(hass, config, action):
|
||||
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
if below is None and above is None:
|
||||
_LOGGER.error("Missing configuration key."
|
||||
@@ -35,13 +44,14 @@ def trigger(hass, config, action):
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return False
|
||||
|
||||
renderer = partial(_renderer, hass, value_template)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
|
||||
"""Listen for state changes and calls action."""
|
||||
# Fire action if we go from outside range into range
|
||||
if _in_range(to_s.state, above, below) and \
|
||||
(from_s is None or not _in_range(from_s.state, above, below)):
|
||||
if _in_range(above, below, renderer(to_s)) and \
|
||||
(from_s is None or not _in_range(above, below, renderer(from_s))):
|
||||
action()
|
||||
|
||||
track_state_change(
|
||||
@@ -51,8 +61,7 @@ def trigger(hass, config, action):
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with state based condition. """
|
||||
|
||||
"""Wrap action method with state based condition."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
@@ -61,6 +70,7 @@ def if_action(hass, config):
|
||||
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
if below is None and above is None:
|
||||
_LOGGER.error("Missing configuration key."
|
||||
@@ -68,21 +78,23 @@ def if_action(hass, config):
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return None
|
||||
|
||||
renderer = partial(_renderer, hass, value_template)
|
||||
|
||||
def if_numeric_state():
|
||||
""" Test numeric state condition. """
|
||||
"""Test numeric state condition."""
|
||||
state = hass.states.get(entity_id)
|
||||
return state is not None and _in_range(state.state, above, below)
|
||||
return state is not None and _in_range(above, below, renderer(state))
|
||||
|
||||
return if_numeric_state
|
||||
|
||||
|
||||
def _in_range(value, range_start, range_end):
|
||||
""" Checks if value is inside the range """
|
||||
|
||||
def _in_range(range_start, range_end, value):
|
||||
"""Check if value is inside the range."""
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
_LOGGER.warn("Missing value in numeric check")
|
||||
_LOGGER.warning("Value returned from template is not a number: %s",
|
||||
value)
|
||||
return False
|
||||
|
||||
if range_start is not None and range_end is not None:
|
||||
|
||||
@@ -1,43 +1,99 @@
|
||||
"""
|
||||
homeassistant.components.automation.state
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers state listening automation rules.
|
||||
Offer state listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#state-trigger
|
||||
"""
|
||||
import 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 configuration."""
|
||||
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):
|
||||
""" Listen for state changes based on `config`. """
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
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()
|
||||
"""Listen for state changes and calls action."""
|
||||
def state_for_listener(now):
|
||||
"""Fire 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):
|
||||
"""Fire on changes and cancel for listener if 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)
|
||||
@@ -46,7 +102,7 @@ def trigger(hass, config, action):
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with state based condition. """
|
||||
"""Wrap action method with state based condition."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
state = config.get(CONF_STATE)
|
||||
|
||||
@@ -56,10 +112,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)
|
||||
"""Test if condition."""
|
||||
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
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
homeassistant.components.automation.sun
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers sun based automation rules.
|
||||
Offer sun based automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#sun-trigger
|
||||
@@ -9,14 +7,18 @@ at https://home-assistant.io/components/automation/#sun-trigger
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components import sun
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import sun
|
||||
from homeassistant.helpers.event import track_sunrise, track_sunset
|
||||
|
||||
DEPENDENCIES = ['sun']
|
||||
|
||||
CONF_OFFSET = 'offset'
|
||||
CONF_EVENT = 'event'
|
||||
CONF_BEFORE = "before"
|
||||
CONF_BEFORE_OFFSET = "before_offset"
|
||||
CONF_AFTER = "after"
|
||||
CONF_AFTER_OFFSET = "after_offset"
|
||||
|
||||
EVENT_SUNSET = 'sunset'
|
||||
EVENT_SUNRISE = 'sunrise'
|
||||
@@ -25,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for events based on config. """
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
if event is None:
|
||||
@@ -37,69 +39,108 @@ def trigger(hass, config, action):
|
||||
_LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event)
|
||||
return False
|
||||
|
||||
if CONF_OFFSET in config:
|
||||
raw_offset = config.get(CONF_OFFSET)
|
||||
|
||||
negative_offset = False
|
||||
if raw_offset.startswith('-'):
|
||||
negative_offset = True
|
||||
raw_offset = raw_offset[1:]
|
||||
|
||||
try:
|
||||
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
|
||||
except ValueError:
|
||||
_LOGGER.error('Could not parse offset %s', raw_offset)
|
||||
return False
|
||||
|
||||
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
if negative_offset:
|
||||
offset *= -1
|
||||
else:
|
||||
offset = timedelta(0)
|
||||
offset = _parse_offset(config.get(CONF_OFFSET))
|
||||
if offset is False:
|
||||
return False
|
||||
|
||||
# Do something to call action
|
||||
if event == EVENT_SUNRISE:
|
||||
trigger_sunrise(hass, action, offset)
|
||||
track_sunrise(hass, action, offset)
|
||||
else:
|
||||
trigger_sunset(hass, action, offset)
|
||||
track_sunset(hass, action, offset)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def trigger_sunrise(hass, action, offset):
|
||||
""" Trigger action at next sun rise. """
|
||||
def next_rise():
|
||||
""" Returns next sunrise. """
|
||||
next_time = sun.next_rising_utc(hass) + offset
|
||||
def if_action(hass, config):
|
||||
"""Wrap action method with sun based condition."""
|
||||
before = config.get(CONF_BEFORE)
|
||||
after = config.get(CONF_AFTER)
|
||||
|
||||
while next_time < dt_util.utcnow():
|
||||
next_time = next_time + timedelta(days=1)
|
||||
# Make sure required configuration keys are present
|
||||
if before is None and after is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing if-condition configuration key %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER)
|
||||
return None
|
||||
|
||||
return next_time
|
||||
# Make sure configuration keys have the right value
|
||||
if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \
|
||||
after not in (None, EVENT_SUNRISE, EVENT_SUNSET):
|
||||
logging.getLogger(__name__).error(
|
||||
"%s and %s can only be set to %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET)
|
||||
return None
|
||||
|
||||
def sunrise_automation_listener(now):
|
||||
""" Called when it's time for action. """
|
||||
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
|
||||
action()
|
||||
before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET))
|
||||
after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET))
|
||||
if before_offset is False or after_offset is False:
|
||||
return None
|
||||
|
||||
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
|
||||
if before is None:
|
||||
def before_func():
|
||||
"""Return no point in time."""
|
||||
return None
|
||||
elif before == EVENT_SUNRISE:
|
||||
def before_func():
|
||||
"""Return time before sunrise."""
|
||||
return sun.next_rising(hass) + before_offset
|
||||
else:
|
||||
def before_func():
|
||||
"""Return time before sunset."""
|
||||
return sun.next_setting(hass) + before_offset
|
||||
|
||||
if after is None:
|
||||
def after_func():
|
||||
"""Return no point in time."""
|
||||
return None
|
||||
elif after == EVENT_SUNRISE:
|
||||
def after_func():
|
||||
"""Return time after sunrise."""
|
||||
return sun.next_rising(hass) + after_offset
|
||||
else:
|
||||
def after_func():
|
||||
"""Return time after sunset."""
|
||||
return sun.next_setting(hass) + after_offset
|
||||
|
||||
def time_if():
|
||||
"""Validate time based if-condition."""
|
||||
now = dt_util.now()
|
||||
before = before_func()
|
||||
after = after_func()
|
||||
|
||||
if before is not None and now > now.replace(hour=before.hour,
|
||||
minute=before.minute):
|
||||
return False
|
||||
|
||||
if after is not None and now < now.replace(hour=after.hour,
|
||||
minute=after.minute):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return time_if
|
||||
|
||||
|
||||
def trigger_sunset(hass, action, offset):
|
||||
""" Trigger action at next sun set. """
|
||||
def next_set():
|
||||
""" Returns next sunrise. """
|
||||
next_time = sun.next_setting_utc(hass) + offset
|
||||
def _parse_offset(raw_offset):
|
||||
"""Parse the offset."""
|
||||
if raw_offset is None:
|
||||
return timedelta(0)
|
||||
|
||||
while next_time < dt_util.utcnow():
|
||||
next_time = next_time + timedelta(days=1)
|
||||
negative_offset = False
|
||||
if raw_offset.startswith('-'):
|
||||
negative_offset = True
|
||||
raw_offset = raw_offset[1:]
|
||||
|
||||
return next_time
|
||||
try:
|
||||
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
|
||||
except ValueError:
|
||||
_LOGGER.error('Could not parse offset %s', raw_offset)
|
||||
return False
|
||||
|
||||
def sunset_automation_listener(now):
|
||||
""" Called when it's time for action. """
|
||||
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
|
||||
action()
|
||||
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
|
||||
if negative_offset:
|
||||
offset *= -1
|
||||
|
||||
return offset
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Offer template automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#template-trigger
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
if value_template is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE)
|
||||
return False
|
||||
|
||||
# Local variable to keep track of if the action has already been triggered
|
||||
already_triggered = False
|
||||
|
||||
def event_listener(event):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal already_triggered
|
||||
template_result = _check_template(hass, value_template)
|
||||
|
||||
# Check to see if template returns true
|
||||
if template_result and not already_triggered:
|
||||
already_triggered = True
|
||||
action()
|
||||
elif not template_result:
|
||||
already_triggered = False
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, event_listener)
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
"""Wrap action method with state based condition."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
if value_template is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE)
|
||||
return False
|
||||
|
||||
return lambda: _check_template(hass, value_template)
|
||||
|
||||
|
||||
def _check_template(hass, value_template):
|
||||
"""Check if result of template is true."""
|
||||
try:
|
||||
value = template.render(hass, value_template, {})
|
||||
except TemplateError:
|
||||
_LOGGER.exception('Error parsing template')
|
||||
return False
|
||||
|
||||
return value.lower() == 'true'
|
||||
@@ -1,14 +1,11 @@
|
||||
"""
|
||||
homeassistant.components.automation.time
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers time listening automation rules.
|
||||
Offer time listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#time-trigger
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
|
||||
@@ -25,25 +22,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AFTER in config:
|
||||
after = dt_util.parse_time_str(config[CONF_AFTER])
|
||||
if after is None:
|
||||
_error_time(config[CONF_AFTER], CONF_AFTER)
|
||||
return False
|
||||
hours, minutes, seconds = after.hour, after.minute, after.second
|
||||
elif (CONF_HOURS in config or CONF_MINUTES in config
|
||||
or CONF_SECONDS in config):
|
||||
hours = convert(config.get(CONF_HOURS), int)
|
||||
minutes = convert(config.get(CONF_MINUTES), int)
|
||||
seconds = convert(config.get(CONF_SECONDS), int)
|
||||
elif (CONF_HOURS in config or CONF_MINUTES in config or
|
||||
CONF_SECONDS in config):
|
||||
hours = config.get(CONF_HOURS)
|
||||
minutes = config.get(CONF_MINUTES)
|
||||
seconds = config.get(CONF_SECONDS)
|
||||
else:
|
||||
_LOGGER.error('One of %s, %s, %s OR %s needs to be specified',
|
||||
CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER)
|
||||
return False
|
||||
|
||||
def time_automation_listener(now):
|
||||
""" Listens for time changes and calls action. """
|
||||
"""Listen for time changes and calls action."""
|
||||
action()
|
||||
|
||||
track_time_change(hass, time_automation_listener,
|
||||
@@ -53,13 +50,13 @@ def trigger(hass, config, action):
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with time based condition. """
|
||||
"""Wrap action method with time based condition."""
|
||||
before = config.get(CONF_BEFORE)
|
||||
after = config.get(CONF_AFTER)
|
||||
weekday = config.get(CONF_WEEKDAY)
|
||||
|
||||
if before is None and after is None and weekday is None:
|
||||
logging.getLogger(__name__).error(
|
||||
_LOGGER.error(
|
||||
"Missing if-condition configuration key %s, %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY)
|
||||
return None
|
||||
@@ -77,7 +74,7 @@ def if_action(hass, config):
|
||||
return None
|
||||
|
||||
def time_if():
|
||||
""" Validate time based if-condition """
|
||||
"""Validate time based if-condition."""
|
||||
now = dt_util.now()
|
||||
if before is not None and now > now.replace(hour=before.hour,
|
||||
minute=before.minute):
|
||||
@@ -100,7 +97,7 @@ def if_action(hass, config):
|
||||
|
||||
|
||||
def _error_time(value, key):
|
||||
""" Helper method to print error. """
|
||||
"""Helper method to print error."""
|
||||
_LOGGER.error(
|
||||
"Received invalid value for '%s': %s", key, value)
|
||||
if isinstance(value, int):
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
homeassistant.components.automation.zone
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers zone automation rules.
|
||||
Offer zone automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#zone-trigger
|
||||
@@ -9,10 +7,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"
|
||||
@@ -23,7 +20,7 @@ DEFAULT_EVENT = EVENT_ENTER
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
@@ -36,7 +33,7 @@ def trigger(hass, config, action):
|
||||
event = config.get(CONF_EVENT, DEFAULT_EVENT)
|
||||
|
||||
def zone_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
"""Listen for state changes and calls action."""
|
||||
if from_s and None in (from_s.attributes.get(ATTR_LATITUDE),
|
||||
from_s.attributes.get(ATTR_LONGITUDE)) or \
|
||||
None in (to_s.attributes.get(ATTR_LATITUDE),
|
||||
@@ -46,6 +43,7 @@ def trigger(hass, config, action):
|
||||
from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None
|
||||
to_match = _in_zone(hass, zone_entity_id, to_s)
|
||||
|
||||
# pylint: disable=too-many-boolean-expressions
|
||||
if event == EVENT_ENTER and not from_match and to_match or \
|
||||
event == EVENT_LEAVE and from_match and not to_match:
|
||||
action()
|
||||
@@ -57,7 +55,7 @@ def trigger(hass, config, action):
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with zone based condition. """
|
||||
"""Wrap action method with zone based condition."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
@@ -68,14 +66,14 @@ def if_action(hass, config):
|
||||
return False
|
||||
|
||||
def if_in_zone():
|
||||
""" Test if condition. """
|
||||
"""Test if condition."""
|
||||
return _in_zone(hass, zone_entity_id, hass.states.get(entity_id))
|
||||
|
||||
return if_in_zone
|
||||
|
||||
|
||||
def _in_zone(hass, zone_entity_id, state):
|
||||
""" Check if state is in zone. """
|
||||
"""Check if state is in zone."""
|
||||
if not state or None in (state.attributes.get(ATTR_LATITUDE),
|
||||
state.attributes.get(ATTR_LONGITUDE)):
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Component to interface with binary sensors.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor/
|
||||
"""
|
||||
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, wemo, 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',
|
||||
wemo.DISCOVER_BINARY_SENSORS: 'wemo',
|
||||
wink.DISCOVER_BINARY_SENSORS: 'wink'
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class BinarySensorDevice(Entity):
|
||||
"""Represent a binary sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
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,41 @@
|
||||
"""
|
||||
Support for tracking the 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):
|
||||
"""Represent UPS online status."""
|
||||
|
||||
def __init__(self, config, data):
|
||||
"""Initialize the APCUPSd device."""
|
||||
self._config = config
|
||||
self._data = data
|
||||
self._state = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the UPS online status sensor."""
|
||||
return self._config.get("name", DEFAULT_NAME)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the UPS is online, else false."""
|
||||
return self._state == apcupsd.VALUE_ONLINE
|
||||
|
||||
def update(self):
|
||||
"""Get the status report from APCUPSd and set this entity's state."""
|
||||
self._state = self._data.status[apcupsd.KEY_STATUS]
|
||||
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
Support for exposed aREST RESTful API of a device.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.arest/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
CONF_RESOURCE = 'resource'
|
||||
CONF_PIN = 'pin'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the aREST binary sensor."""
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
pin = config.get(CONF_PIN)
|
||||
|
||||
if None in (resource, pin):
|
||||
_LOGGER.error('Not all required config keys present: %s',
|
||||
', '.join((CONF_RESOURCE, CONF_PIN)))
|
||||
return False
|
||||
|
||||
try:
|
||||
response = requests.get(resource, timeout=10).json()
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error('Missing resource or schema in configuration. '
|
||||
'Add http:// to your URL.')
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('No route to device at %s. '
|
||||
'Please check the IP address in the configuration file.',
|
||||
resource)
|
||||
return False
|
||||
|
||||
arest = ArestData(resource, pin)
|
||||
|
||||
add_devices([ArestBinarySensor(arest,
|
||||
resource,
|
||||
config.get('name', response['name']),
|
||||
pin)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
"""Implement an aREST binary sensor for a pin."""
|
||||
|
||||
def __init__(self, arest, resource, name, pin):
|
||||
"""Initialize the aREST device."""
|
||||
self.arest = arest
|
||||
self._resource = resource
|
||||
self._name = name
|
||||
self._pin = pin
|
||||
self.update()
|
||||
|
||||
if self._pin is not None:
|
||||
request = requests.get('{}/mode/{}/i'.format
|
||||
(self._resource, self._pin), timeout=10)
|
||||
if request.status_code is not 200:
|
||||
_LOGGER.error("Can't set mode. Is device offline?")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return bool(self.arest.data.get('state'))
|
||||
|
||||
def update(self):
|
||||
"""Get 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."""
|
||||
|
||||
def __init__(self, resource, pin):
|
||||
"""Initialize the aREST data object."""
|
||||
self._resource = resource
|
||||
self._pin = pin
|
||||
self.data = {}
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from aREST device."""
|
||||
try:
|
||||
response = requests.get('{}/digital/{}'.format(
|
||||
self._resource, self._pin), timeout=10)
|
||||
self.data = {'state': response.json()['return_value']}
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device '%s'. Is device offline?",
|
||||
self._resource)
|
||||
@@ -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):
|
||||
"""Setup 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):
|
||||
"""Represent 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):
|
||||
"""Return the 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):
|
||||
"""Return true 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]
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Support for custom shell commands to to retrieve values.
|
||||
|
||||
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.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.command_line import CommandSensorData
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.helpers import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Binary Command Sensor"
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Command Sensor."""
|
||||
if config.get('command') is None:
|
||||
_LOGGER.error('Missing required variable: "command"')
|
||||
return False
|
||||
|
||||
data = CommandSensorData(config.get('command'))
|
||||
|
||||
add_devices([CommandBinarySensor(
|
||||
hass,
|
||||
data,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
||||
config.get(CONF_VALUE_TEMPLATE)
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class CommandBinarySensor(BinarySensorDevice):
|
||||
"""Represent a command line binary sensor."""
|
||||
|
||||
def __init__(self, hass, data, name, payload_on,
|
||||
payload_off, value_template):
|
||||
"""Initialize the Command line binary sensor."""
|
||||
self._hass = hass
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._value_template = value_template
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
self.data.update()
|
||||
value = self.data.value
|
||||
|
||||
if self._value_template is not None:
|
||||
value = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, value, False)
|
||||
if value == self._payload_on:
|
||||
self._state = True
|
||||
elif value == self._payload_off:
|
||||
self._state = False
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
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):
|
||||
"""Setup the Demo binary sensor platform."""
|
||||
add_devices([
|
||||
DemoBinarySensor('Basement Floor Wet', False, 'moisture'),
|
||||
DemoBinarySensor('Movement Backyard', True, 'motion'),
|
||||
])
|
||||
|
||||
|
||||
class DemoBinarySensor(BinarySensorDevice):
|
||||
"""A Demo binary sensor."""
|
||||
|
||||
def __init__(self, name, state, sensor_class):
|
||||
"""Initialize the demo sensor."""
|
||||
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."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Support for MQTT binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mqtt/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
SENSOR_CLASSES)
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.helpers import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'MQTT Binary sensor'
|
||||
DEFAULT_QOS = 0
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Add MQTT binary sensor."""
|
||||
if config.get('state_topic') is None:
|
||||
_LOGGER.error('Missing required variable: state_topic')
|
||||
return False
|
||||
|
||||
sensor_class = config.get('sensor_class')
|
||||
if sensor_class not in SENSOR_CLASSES:
|
||||
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
|
||||
sensor_class = None
|
||||
|
||||
add_devices([MqttBinarySensor(
|
||||
hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('state_topic', None),
|
||||
sensor_class,
|
||||
config.get('qos', DEFAULT_QOS),
|
||||
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttBinarySensor(BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
def __init__(self, hass, name, state_topic, sensor_class, qos, payload_on,
|
||||
payload_off, value_template):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._state_topic = state_topic
|
||||
self._sensor_class = sensor_class
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._qos = qos
|
||||
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = template.render_with_possible_json_value(
|
||||
hass, value_template, payload)
|
||||
if payload == self._payload_on:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
elif payload == self._payload_off:
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
|
||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
@@ -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
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Support for Nest Thermostat Binary Sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.nest/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import homeassistant.components.nest as nest
|
||||
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',
|
||||
'hvac_alt_heat_x2_state',
|
||||
'hvac_emer_heat_state',
|
||||
'online']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Nest binary sensors."""
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
for structure in nest.NEST.structures:
|
||||
for device in structure.devices:
|
||||
for variable in config['monitored_conditions']:
|
||||
if variable in BINARY_TYPES:
|
||||
add_devices([NestBinarySensor(structure,
|
||||
device,
|
||||
variable)])
|
||||
else:
|
||||
logger.error('Nest sensor type: "%s" does not exist',
|
||||
variable)
|
||||
except socket.error:
|
||||
logger.error(
|
||||
"Connection error logging into the nest web service."
|
||||
)
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
"""Represents a Nest binary sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the binary sensor is on."""
|
||||
return bool(getattr(self.device, self.variable))
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
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):
|
||||
"""Initialize the nx594 binary sensor."""
|
||||
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):
|
||||
"""Return the 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):
|
||||
"""Initialize nx584 watcher thread."""
|
||||
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):
|
||||
"""Run the watcher."""
|
||||
while True:
|
||||
try:
|
||||
self._run()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('Failed to reach NX584 server')
|
||||
time.sleep(10)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Support for RESTful binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
import logging
|
||||
|
||||
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__)
|
||||
|
||||
DEFAULT_NAME = 'REST Binary Sensor'
|
||||
DEFAULT_METHOD = 'GET'
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup REST binary sensors."""
|
||||
resource = config.get('resource', None)
|
||||
method = config.get('method', DEFAULT_METHOD)
|
||||
payload = config.get('payload', None)
|
||||
verify_ssl = config.get('verify_ssl', True)
|
||||
|
||||
rest = RestData(method, resource, payload, verify_ssl)
|
||||
rest.update()
|
||||
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch Rest data')
|
||||
return False
|
||||
|
||||
add_devices([RestBinarySensor(
|
||||
hass, rest, config.get('name', DEFAULT_NAME),
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
"""A REST binary sensor."""
|
||||
|
||||
def __init__(self, hass, rest, name, value_template):
|
||||
"""Initialize a REST binary sensor."""
|
||||
self._hass = hass
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._value_template = value_template
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
if self.rest.data is None:
|
||||
return False
|
||||
|
||||
if self._value_template is not None:
|
||||
self.rest.data = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
return bool(int(self.rest.data))
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from REST API and updates the state."""
|
||||
self.rest.update()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Support for 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
|
||||
|
||||
DEFAULT_PULL_MODE = "UP"
|
||||
DEFAULT_BOUNCETIME = 50
|
||||
DEFAULT_INVERT_LOGIC = False
|
||||
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Raspberry PI GPIO devices."""
|
||||
pull_mode = config.get('pull_mode', DEFAULT_PULL_MODE)
|
||||
bouncetime = config.get('bouncetime', DEFAULT_BOUNCETIME)
|
||||
invert_logic = config.get('invert_logic', DEFAULT_INVERT_LOGIC)
|
||||
|
||||
binary_sensors = []
|
||||
ports = config.get('ports')
|
||||
for port_num, port_name in ports.items():
|
||||
binary_sensors.append(RPiGPIOBinarySensor(
|
||||
port_name, port_num, pull_mode, bouncetime, invert_logic))
|
||||
add_devices(binary_sensors)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
"""Represent a binary sensor that uses Raspberry Pi GPIO."""
|
||||
|
||||
def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
|
||||
"""Initialize the RPi binary sensor."""
|
||||
# pylint: disable=no-member
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._port = port
|
||||
self._pull_mode = pull_mode
|
||||
self._bouncetime = bouncetime
|
||||
self._invert_logic = invert_logic
|
||||
|
||||
rpi_gpio.setup_input(self._port, self._pull_mode)
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
|
||||
def read_gpio(port):
|
||||
"""Read 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."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state != self._invert_logic
|
||||
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
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,130 @@
|
||||
"""
|
||||
Support for exposing a templated binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.template/
|
||||
"""
|
||||
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):
|
||||
"""Initialize the Template binary sensor."""
|
||||
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):
|
||||
if not hasattr(self, 'hass'):
|
||||
return
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._sensor_class
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
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,75 @@
|
||||
"""
|
||||
Support for WeMo sensors.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.wemo/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['wemo']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, too-many-function-args
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Register discovered WeMo binary sensors."""
|
||||
import pywemo.discovery as discovery
|
||||
|
||||
if discovery_info is not None:
|
||||
location = discovery_info[2]
|
||||
mac = discovery_info[3]
|
||||
device = discovery.device_from_description(location, mac)
|
||||
|
||||
if device:
|
||||
add_devices_callback([WemoBinarySensor(device)])
|
||||
|
||||
|
||||
class WemoBinarySensor(BinarySensorDevice):
|
||||
"""Represents a WeMo binary sensor."""
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the WeMo sensor."""
|
||||
self.wemo = device
|
||||
self._state = None
|
||||
|
||||
wemo = get_component('wemo')
|
||||
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
|
||||
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)
|
||||
|
||||
def _update_callback(self, _device, _params):
|
||||
"""Called by the wemo device callback to update state."""
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s',
|
||||
_device)
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed with subscriptions."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of this WeMo device."""
|
||||
return "{}.{}".format(self.__class__, self.wemo.serialnumber)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sevice if any."""
|
||||
return self.wemo.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update WeMo state."""
|
||||
try:
|
||||
self._state = self.wemo.get_state(True)
|
||||
except AttributeError:
|
||||
_LOGGER.warning('Could not update status for %s', self.name)
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
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):
|
||||
"""Setup 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):
|
||||
"""Representation of a Wink sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
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):
|
||||
"""Return the ID of this wink sensor."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update state of the sensor."""
|
||||
self.wink.update_state()
|
||||
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
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."""
|
||||
add_entities([
|
||||
ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))
|
||||
])
|
||||
|
||||
|
||||
class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice):
|
||||
"""Use ZigBeeDigitalIn as binary sensor."""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
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)
|
||||
WENZHOU = 0x0118
|
||||
WENZHOU_SLIM_SENSOR_MOTION = (WENZHOU, PHILIO_SLIM_SENSOR, 0)
|
||||
|
||||
WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
|
||||
WENZHOU_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]]
|
||||
value.set_change_verified(False)
|
||||
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16),
|
||||
value.index)
|
||||
|
||||
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)
|
||||
])
|
||||
return
|
||||
|
||||
if value.command_class == COMMAND_CLASS_SENSOR_BINARY:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
|
||||
|
||||
class ZWaveBinarySensor(BinarySensorDevice, ZWaveDeviceEntity):
|
||||
"""Representation of a binary sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, sensor_class):
|
||||
"""Initialize the sensor."""
|
||||
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):
|
||||
"""Representation of a stateless sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, sensor_value, sensor_class, hass, re_arm_sec=60):
|
||||
"""Initialize the sensor."""
|
||||
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,85 @@
|
||||
"""
|
||||
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):
|
||||
"""Initialize the BookSky."""
|
||||
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):
|
||||
"""Use the API to retreive a list of devices."""
|
||||
_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,22 +1,16 @@
|
||||
"""
|
||||
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"
|
||||
DEPENDENCIES = []
|
||||
|
||||
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."""
|
||||
import webbrowser
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_BROWSE_URL,
|
||||
|
||||
@@ -1,56 +1,40 @@
|
||||
# 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/
|
||||
"""
|
||||
import requests
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
import time
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
|
||||
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'
|
||||
@@ -58,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 sensors. """
|
||||
|
||||
"""Setup the camera component."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
@@ -78,21 +61,23 @@ 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)
|
||||
|
||||
camera = None
|
||||
if entity_id in component.entities.keys():
|
||||
camera = component.entities[entity_id]
|
||||
|
||||
if camera:
|
||||
response = camera.camera_image()
|
||||
if response is not None:
|
||||
handler.wfile.write(response)
|
||||
else:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
else:
|
||||
if camera is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
response = camera.camera_image()
|
||||
|
||||
if response is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
handler.wfile.write(response)
|
||||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
@@ -101,18 +86,17 @@ 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.
|
||||
"""
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
camera = component.entities.get(entity_id)
|
||||
|
||||
camera = None
|
||||
if entity_id in component.entities.keys():
|
||||
camera = component.entities[entity_id]
|
||||
|
||||
if not camera:
|
||||
if camera is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
@@ -120,39 +104,12 @@ 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'))
|
||||
camera.mjpeg_stream(handler)
|
||||
|
||||
except (requests.RequestException, IOError):
|
||||
camera.is_streaming = False
|
||||
camera.update_ha_state()
|
||||
|
||||
camera.is_streaming = False
|
||||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(
|
||||
@@ -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,61 @@
|
||||
"""
|
||||
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):
|
||||
"""Setup 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):
|
||||
"""Representation of the images published from the BloomSky's camera."""
|
||||
|
||||
def __init__(self, bs, device):
|
||||
"""Setup 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):
|
||||
"""Return the name of this BloomSky device."""
|
||||
return self._name
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
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
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo camera platform."""
|
||||
add_devices([
|
||||
DemoCamera('Demo camera')
|
||||
])
|
||||
|
||||
|
||||
class DemoCamera(Camera):
|
||||
"""A Demo camera."""
|
||||
|
||||
def __init__(self, name):
|
||||
"""Initialize demo camera component."""
|
||||
super().__init__()
|
||||
self._name = name
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a faked still image response."""
|
||||
now = dt_util.utcnow()
|
||||
|
||||
image_path = os.path.join(os.path.dirname(__file__),
|
||||
'demo_{}.jpg'.format(now.second % 4))
|
||||
with open(image_path, 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -1,23 +1,22 @@
|
||||
"""
|
||||
homeassistant.components.camera.foscam
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
This component provides basic support for Foscam IP cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.foscam/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Adds a Foscam IP Camera. """
|
||||
"""Setup a Foscam IP Camera."""
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: ['username', 'password', 'ip']}, _LOGGER):
|
||||
return None
|
||||
@@ -27,9 +26,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class FoscamCamera(Camera):
|
||||
""" An implementation of a Foscam IP camera. """
|
||||
"""An implementation of a Foscam IP camera."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
"""Initialize a Foscam camera."""
|
||||
super(FoscamCamera, self).__init__()
|
||||
|
||||
ip_address = device_info.get('ip')
|
||||
@@ -47,8 +47,7 @@ class FoscamCamera(Camera):
|
||||
self._name, self._snap_picture_url)
|
||||
|
||||
def camera_image(self):
|
||||
""" Return a still image reponse from the camera. """
|
||||
|
||||
"""Return a still image reponse from the camera."""
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = requests.get(self._snap_picture_url)
|
||||
|
||||
@@ -56,5 +55,5 @@ class FoscamCamera(Camera):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Return the name of this device. """
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
"""
|
||||
homeassistant.components.camera.generic
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for IP Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.generic/
|
||||
"""
|
||||
import logging
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Adds a generic IP Camera. """
|
||||
"""Setup a generic IP Camera."""
|
||||
if not validate_config({DOMAIN: config}, {DOMAIN: ['still_image_url']},
|
||||
_LOGGER):
|
||||
return None
|
||||
@@ -28,11 +27,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GenericCamera(Camera):
|
||||
"""
|
||||
A generic implementation of an IP camera that is reachable over a URL.
|
||||
"""
|
||||
"""A generic implementation of an IP camera."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
"""Initialize a generic camera."""
|
||||
super().__init__()
|
||||
self._name = device_info.get('name', 'Generic Camera')
|
||||
self._username = device_info.get('username')
|
||||
@@ -40,7 +38,7 @@ class GenericCamera(Camera):
|
||||
self._still_image_url = device_info['still_image_url']
|
||||
|
||||
def camera_image(self):
|
||||
""" Return a still image reponse from the camera. """
|
||||
"""Return a still image response from the camera."""
|
||||
if self._username and self._password:
|
||||
try:
|
||||
response = requests.get(
|
||||
@@ -60,5 +58,5 @@ class GenericCamera(Camera):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Return the name of this device. """
|
||||
"""Return the name of this device."""
|
||||
return self._name
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
"""
|
||||
homeassistant.components.camera.mjpeg
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for IP Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.mjpeg/
|
||||
"""
|
||||
import logging
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
import requests
|
||||
from contextlib import closing
|
||||
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Adds a mjpeg IP Camera. """
|
||||
"""Setup a MJPEG IP Camera."""
|
||||
if not validate_config({DOMAIN: config}, {DOMAIN: ['mjpeg_url']},
|
||||
_LOGGER):
|
||||
return None
|
||||
@@ -29,22 +31,31 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class MjpegCamera(Camera):
|
||||
"""
|
||||
A generic implementation of an IP camera that is reachable over a URL.
|
||||
"""
|
||||
"""An implementation of an IP camera that is reachable over a URL."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
"""Initialize a MJPEG camera."""
|
||||
super().__init__()
|
||||
self._name = device_info.get('name', 'Mjpeg Camera')
|
||||
self._username = device_info.get('username')
|
||||
self._password = device_info.get('password')
|
||||
self._mjpeg_url = device_info['mjpeg_url']
|
||||
|
||||
def camera_image(self):
|
||||
""" Return a still image response from the camera. """
|
||||
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."""
|
||||
def process_response(response):
|
||||
""" Take in a response object, return the jpg from it. """
|
||||
"""Take in a response object, return the jpg from it."""
|
||||
data = b''
|
||||
for chunk in response.iter_content(1024):
|
||||
data += chunk
|
||||
@@ -54,18 +65,24 @@ 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):
|
||||
""" Return the name of this device. """
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
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):
|
||||
"""Initialize an Unifi camera."""
|
||||
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 the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the camera is recording."""
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
return caminfo['recordingSettings']['fullTimeRecordEnabled']
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the brand of this camera."""
|
||||
return 'Ubiquiti'
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the model of this camera."""
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
return caminfo['model']
|
||||
|
||||
def _login(self):
|
||||
"""Login to the camera."""
|
||||
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):
|
||||
"""Return the image of this camera."""
|
||||
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()
|
||||
@@ -1,8 +1,5 @@
|
||||
"""
|
||||
homeassistant.components.configurator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A component to allow pieces of code to request configuration from the user.
|
||||
Support to allow pieces of code to request configuration from the user.
|
||||
|
||||
Initiate a request by calling the `request_config` method with a callback.
|
||||
This will return a request id that has to be used for future calls.
|
||||
@@ -11,11 +8,10 @@ the user has submitted configuration information.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import generate_entity_id
|
||||
from homeassistant.const import EVENT_TIME_CHANGED
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
DOMAIN = "configurator"
|
||||
DEPENDENCIES = []
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
SERVICE_CONFIGURE = "configure"
|
||||
@@ -39,9 +35,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def request_config(
|
||||
hass, name, callback, description=None, description_image=None,
|
||||
submit_caption=None, fields=None):
|
||||
""" Create a new request for config.
|
||||
Will return an ID to be used for sequent calls. """
|
||||
"""Create a new request for configuration.
|
||||
|
||||
Will return an ID to be used for sequent calls.
|
||||
"""
|
||||
instance = _get_instance(hass)
|
||||
|
||||
request_id = instance.request_config(
|
||||
@@ -54,7 +51,7 @@ def request_config(
|
||||
|
||||
|
||||
def notify_errors(request_id, error):
|
||||
""" Add errors to a config request. """
|
||||
"""Add errors to a config request."""
|
||||
try:
|
||||
_REQUESTS[request_id].notify_errors(request_id, error)
|
||||
except KeyError:
|
||||
@@ -63,7 +60,7 @@ def notify_errors(request_id, error):
|
||||
|
||||
|
||||
def request_done(request_id):
|
||||
""" Mark a config request as done. """
|
||||
"""Mark a configuration request as done."""
|
||||
try:
|
||||
_REQUESTS.pop(request_id).request_done(request_id)
|
||||
except KeyError:
|
||||
@@ -72,12 +69,12 @@ def request_done(request_id):
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Set up Configurator. """
|
||||
"""Setup the configurator component."""
|
||||
return True
|
||||
|
||||
|
||||
def _get_instance(hass):
|
||||
""" Get an instance per hass object. """
|
||||
"""Get an instance per hass object."""
|
||||
try:
|
||||
return _INSTANCES[hass]
|
||||
except KeyError:
|
||||
@@ -90,11 +87,10 @@ def _get_instance(hass):
|
||||
|
||||
|
||||
class Configurator(object):
|
||||
"""
|
||||
Class to keep track of current configuration requests.
|
||||
"""
|
||||
"""The class to keep track of current configuration requests."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the configurator."""
|
||||
self.hass = hass
|
||||
self._cur_id = 0
|
||||
self._requests = {}
|
||||
@@ -105,8 +101,7 @@ class Configurator(object):
|
||||
def request_config(
|
||||
self, name, callback,
|
||||
description, description_image, submit_caption, fields):
|
||||
""" Setup a request for configuration. """
|
||||
|
||||
"""Setup a request for configuration."""
|
||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
|
||||
|
||||
if fields is None:
|
||||
@@ -134,7 +129,7 @@ class Configurator(object):
|
||||
return request_id
|
||||
|
||||
def notify_errors(self, request_id, error):
|
||||
""" Update the state with errors. """
|
||||
"""Update the state with errors."""
|
||||
if not self._validate_request_id(request_id):
|
||||
return
|
||||
|
||||
@@ -142,13 +137,13 @@ 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)
|
||||
|
||||
def request_done(self, request_id):
|
||||
""" Remove the config request. """
|
||||
"""Remove the configuration request."""
|
||||
if not self._validate_request_id(request_id):
|
||||
return
|
||||
|
||||
@@ -161,13 +156,13 @@ class Configurator(object):
|
||||
self.hass.states.set(entity_id, STATE_CONFIGURED)
|
||||
|
||||
def deferred_remove(event):
|
||||
""" Remove the request state. """
|
||||
"""Remove the request state."""
|
||||
self.hass.states.remove(entity_id)
|
||||
|
||||
self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove)
|
||||
|
||||
def handle_service_call(self, call):
|
||||
""" Handle a configure service call. """
|
||||
"""Handle a configure service call."""
|
||||
request_id = call.data.get(ATTR_CONFIGURE_ID)
|
||||
|
||||
if not self._validate_request_id(request_id):
|
||||
@@ -181,10 +176,10 @@ class Configurator(object):
|
||||
callback(call.data.get(ATTR_FIELDS, {}))
|
||||
|
||||
def _generate_unique_id(self):
|
||||
""" Generates a unique configurator id. """
|
||||
"""Generate a unique configurator ID."""
|
||||
self._cur_id += 1
|
||||
return "{}-{}".format(id(self), self._cur_id)
|
||||
|
||||
def _validate_request_id(self, request_id):
|
||||
""" Validate that the request belongs to this instance. """
|
||||
"""Validate that the request belongs to this instance."""
|
||||
return request_id in self._requests
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
homeassistant.components.conversation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides functionality to have conversations with Home Assistant.
|
||||
Support for functionality to have conversations with Home Assistant.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/conversation/
|
||||
@@ -11,10 +9,9 @@ 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"
|
||||
DEPENDENCIES = []
|
||||
|
||||
SERVICE_PROCESS = "process"
|
||||
|
||||
@@ -22,19 +19,22 @@ ATTR_TEXT = "text"
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.8.0']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Registers the process service. """
|
||||
"""Register the process service."""
|
||||
from fuzzywuzzy import process as fuzzyExtract
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def process(service):
|
||||
""" Parses text into commands for Home Assistant. """
|
||||
"""Parse text into commands."""
|
||||
if ATTR_TEXT not in service.data:
|
||||
logger.error("Received process service call without a text")
|
||||
return
|
||||
|
||||
text = service.data[ATTR_TEXT].lower()
|
||||
|
||||
match = REGEX_TURN_COMMAND.match(text)
|
||||
|
||||
if not match:
|
||||
@@ -42,10 +42,9 @@ def setup(hass, config):
|
||||
return
|
||||
|
||||
name, command = match.groups()
|
||||
|
||||
entity_ids = [
|
||||
state.entity_id for state in hass.states.all()
|
||||
if state.name.lower() == name]
|
||||
entities = {state.entity_id: state.name for state in hass.states.all()}
|
||||
entity_ids = fuzzyExtract.extractOne(name, entities,
|
||||
score_cutoff=65)[2]
|
||||
|
||||
if not entity_ids:
|
||||
logger.error(
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
"""
|
||||
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"
|
||||
|
||||
DEPENDENCIES = ['conversation', 'introduction', 'zone']
|
||||
|
||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'device_tracker', 'light', 'media_player', 'notify', 'switch', 'sensor',
|
||||
'thermostat']
|
||||
'alarm_control_panel',
|
||||
'binary_sensor',
|
||||
'camera',
|
||||
'device_tracker',
|
||||
'garage_door',
|
||||
'light',
|
||||
'lock',
|
||||
'media_player',
|
||||
'notify',
|
||||
'rollershutter',
|
||||
'sensor',
|
||||
'switch',
|
||||
'thermostat',
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup a demo environment. """
|
||||
"""Setup a demo environment."""
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
|
||||
@@ -42,35 +53,40 @@ def setup(hass, config):
|
||||
bootstrap.setup_component(hass, 'sun')
|
||||
|
||||
# Setup demo platforms
|
||||
demo_config = config.copy()
|
||||
for component in COMPONENTS_WITH_DEMO_PLATFORM:
|
||||
bootstrap.setup_component(
|
||||
hass, component, {component: {CONF_PLATFORM: 'demo'}})
|
||||
demo_config[component] = {CONF_PLATFORM: 'demo'}
|
||||
bootstrap.setup_component(hass, component, demo_config)
|
||||
|
||||
# Setup room groups
|
||||
lights = sorted(hass.states.entity_ids('light'))
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
media_players = sorted(hass.states.entity_ids('media_player'))
|
||||
group.setup_group(hass, 'living room', [lights[2], lights[1], switches[0],
|
||||
media_players[1]])
|
||||
group.setup_group(hass, 'bedroom', [lights[0], switches[1],
|
||||
media_players[0]])
|
||||
|
||||
# Setup IP Camera
|
||||
bootstrap.setup_component(
|
||||
hass, 'camera',
|
||||
{'camera': {
|
||||
'platform': 'generic',
|
||||
'name': 'IP Camera',
|
||||
'still_image_url': 'http://home-assistant.io/demo/webcam.jpg',
|
||||
}})
|
||||
|
||||
# Setup alarm_control_panel
|
||||
bootstrap.setup_component(
|
||||
hass, 'alarm_control_panel',
|
||||
{'alarm_control_panel': {
|
||||
'platform': 'manual',
|
||||
'name': 'Test Alarm',
|
||||
}})
|
||||
group.Group(hass, 'living room', [
|
||||
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, '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', 'group.doors', 'thermostat.nest',
|
||||
], view=True)
|
||||
group.Group(hass, 'Upstairs', [
|
||||
'thermostat.thermostat', 'group.bedroom',
|
||||
], view=True)
|
||||
|
||||
# Setup scripts
|
||||
bootstrap.setup_component(
|
||||
@@ -111,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.
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""
|
||||
homeassistant.components.device_sun_light_trigger
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides functionality to turn on lights based on the state of the sun and
|
||||
devices.
|
||||
Provides functionality to turn on lights based on the states.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_sun_light_trigger/
|
||||
@@ -10,10 +7,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
|
||||
from homeassistant.helpers.event_decorators import track_state_change
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DOMAIN = "device_sun_light_trigger"
|
||||
DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun']
|
||||
@@ -28,28 +26,26 @@ CONF_LIGHT_GROUP = 'light_group'
|
||||
CONF_DEVICE_GROUP = 'device_group'
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-locals
|
||||
def setup(hass, config):
|
||||
""" Triggers to turn lights on or off based on device precense. """
|
||||
"""The triggers to turn lights on or off based on device presence."""
|
||||
logger = logging.getLogger(__name__)
|
||||
device_tracker = get_component('device_tracker')
|
||||
group = get_component('group')
|
||||
light = get_component('light')
|
||||
sun = get_component('sun')
|
||||
|
||||
disable_turn_off = 'disable_turn_off' in config[DOMAIN]
|
||||
|
||||
light_group = config[DOMAIN].get(CONF_LIGHT_GROUP,
|
||||
light.ENTITY_ID_ALL_LIGHTS)
|
||||
|
||||
light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE, LIGHT_PROFILE)
|
||||
|
||||
device_group = config[DOMAIN].get(CONF_DEVICE_GROUP,
|
||||
device_tracker.ENTITY_ID_ALL_DEVICES)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
device_entity_ids = group.get_entity_ids(hass, device_group,
|
||||
device_tracker.DOMAIN)
|
||||
|
||||
if not device_entity_ids:
|
||||
logger.error("No devices found to track")
|
||||
|
||||
return False
|
||||
|
||||
# Get the light IDs from the specified group
|
||||
@@ -57,116 +53,105 @@ def setup(hass, config):
|
||||
|
||||
if not light_ids:
|
||||
logger.error("No lights found to turn on ")
|
||||
|
||||
return False
|
||||
|
||||
def calc_time_for_light_when_sunset():
|
||||
""" Calculates the time when to start fading lights in when sun sets.
|
||||
Returns None if no next_setting data available. """
|
||||
"""Calculate the time when to start fading lights in when sun sets.
|
||||
|
||||
Returns None if no next_setting data available.
|
||||
"""
|
||||
next_setting = sun.next_setting(hass)
|
||||
|
||||
if next_setting:
|
||||
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
|
||||
else:
|
||||
if not next_setting:
|
||||
return None
|
||||
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
|
||||
|
||||
def schedule_light_on_sun_rise(entity, old_state, new_state):
|
||||
"""The moment sun sets we want to have all the lights on.
|
||||
We will schedule to have each light start after one another
|
||||
and slowly transition in."""
|
||||
def turn_light_on_before_sunset(light_id):
|
||||
"""Helper function to turn on lights.
|
||||
|
||||
def turn_light_on_before_sunset(light_id):
|
||||
""" Helper function to turn on lights slowly if there
|
||||
are devices home and the light is not on yet. """
|
||||
if device_tracker.is_on(hass) and not light.is_on(hass, light_id):
|
||||
|
||||
light.turn_on(hass, light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
|
||||
def turn_on(light_id):
|
||||
""" Lambda can keep track of function parameters but not local
|
||||
parameters. If we put the lambda directly in the below statement
|
||||
only the last light will be turned on.. """
|
||||
return lambda now: turn_light_on_before_sunset(light_id)
|
||||
|
||||
start_point = calc_time_for_light_when_sunset()
|
||||
|
||||
if start_point:
|
||||
for index, light_id in enumerate(light_ids):
|
||||
track_point_in_time(
|
||||
hass, turn_on(light_id),
|
||||
(start_point + index * LIGHT_TRANSITION_TIME))
|
||||
Speed is slow if there are devices home and the light is not on yet.
|
||||
"""
|
||||
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
|
||||
return
|
||||
light.turn_on(hass, light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
|
||||
# Track every time sun rises so we can schedule a time-based
|
||||
# pre-sun set event
|
||||
track_state_change(hass, sun.ENTITY_ID, schedule_light_on_sun_rise,
|
||||
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
|
||||
@track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON,
|
||||
sun.STATE_ABOVE_HORIZON)
|
||||
def schedule_lights_at_sun_set(hass, entity, old_state, new_state):
|
||||
"""The moment sun sets we want to have all the lights on.
|
||||
|
||||
# If the sun is already above horizon
|
||||
# schedule the time-based pre-sun set event
|
||||
We will schedule to have each light start after one another
|
||||
and slowly transition in.
|
||||
"""
|
||||
start_point = calc_time_for_light_when_sunset()
|
||||
if not start_point:
|
||||
return
|
||||
|
||||
def turn_on(light_id):
|
||||
"""Lambda can keep track of function parameters.
|
||||
|
||||
No local parameters. If we put the lambda directly in the below
|
||||
statement only the last light will be turned on.
|
||||
"""
|
||||
return lambda now: turn_light_on_before_sunset(light_id)
|
||||
|
||||
for index, light_id in enumerate(light_ids):
|
||||
track_point_in_time(hass, turn_on(light_id),
|
||||
start_point + index * LIGHT_TRANSITION_TIME)
|
||||
|
||||
# If the sun is already above horizon schedule the time-based pre-sun set
|
||||
# event.
|
||||
if sun.is_on(hass):
|
||||
schedule_light_on_sun_rise(None, None, None)
|
||||
schedule_lights_at_sun_set(hass, None, None, None)
|
||||
|
||||
def check_light_on_dev_state_change(entity, old_state, new_state):
|
||||
""" Function to handle tracked device state changes. """
|
||||
@track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME)
|
||||
def check_light_on_dev_state_change(hass, entity, old_state, new_state):
|
||||
"""Handle tracked device state changes."""
|
||||
# pylint: disable=unused-variable
|
||||
lights_are_on = group.is_on(hass, light_group)
|
||||
|
||||
light_needed = not (lights_are_on or sun.is_on(hass))
|
||||
|
||||
# Specific device came home ?
|
||||
if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \
|
||||
new_state.state == STATE_HOME:
|
||||
# These variables are needed for the elif check
|
||||
now = dt_util.now()
|
||||
start_point = calc_time_for_light_when_sunset()
|
||||
|
||||
# These variables are needed for the elif check
|
||||
now = dt_util.now()
|
||||
start_point = calc_time_for_light_when_sunset()
|
||||
# Do we need lights?
|
||||
if light_needed:
|
||||
logger.info("Home coming event for %s. Turning lights on", entity)
|
||||
light.turn_on(hass, light_ids, profile=light_profile)
|
||||
|
||||
# Do we need lights?
|
||||
if light_needed:
|
||||
# Are we in the time span were we would turn on the lights
|
||||
# if someone would be home?
|
||||
# Check this by seeing if current time is later then the point
|
||||
# in time when we would start putting the lights on.
|
||||
elif (start_point and
|
||||
start_point < now < sun.next_setting(hass)):
|
||||
|
||||
logger.info(
|
||||
"Home coming event for %s. Turning lights on", entity)
|
||||
# Check for every light if it would be on if someone was home
|
||||
# when the fading in started and turn it on if so
|
||||
for index, light_id in enumerate(light_ids):
|
||||
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
||||
light.turn_on(hass, light_id)
|
||||
|
||||
light.turn_on(hass, light_ids, profile=light_profile)
|
||||
else:
|
||||
# If this light didn't happen to be turned on yet so
|
||||
# will all the following then, break.
|
||||
break
|
||||
|
||||
# Are we in the time span were we would turn on the lights
|
||||
# if someone would be home?
|
||||
# Check this by seeing if current time is later then the point
|
||||
# in time when we would start putting the lights on.
|
||||
elif (start_point and
|
||||
start_point < now < sun.next_setting(hass)):
|
||||
|
||||
# Check for every light if it would be on if someone was home
|
||||
# when the fading in started and turn it on if so
|
||||
for index, light_id in enumerate(light_ids):
|
||||
|
||||
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
||||
light.turn_on(hass, light_id)
|
||||
|
||||
else:
|
||||
# If this light didn't happen to be turned on yet so
|
||||
# will all the following then, break.
|
||||
break
|
||||
|
||||
# Did all devices leave the house?
|
||||
elif (entity == device_group and
|
||||
new_state.state == STATE_NOT_HOME and lights_are_on and
|
||||
not disable_turn_off):
|
||||
if not disable_turn_off:
|
||||
@track_state_change(device_group, STATE_HOME, STATE_NOT_HOME)
|
||||
def turn_off_lights_when_all_leave(hass, entity, old_state, new_state):
|
||||
"""Handle device group state change."""
|
||||
# pylint: disable=unused-variable
|
||||
if not group.is_on(hass, light_group):
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"Everyone has left but there are lights on. Turning them off")
|
||||
|
||||
light.turn_off(hass, light_ids)
|
||||
|
||||
# Track home coming of each device
|
||||
track_state_change(
|
||||
hass, device_entity_ids, check_light_on_dev_state_change,
|
||||
STATE_NOT_HOME, STATE_HOME)
|
||||
|
||||
# Track when all devices are gone to shut down lights
|
||||
track_state_change(
|
||||
hass, device_group, check_light_on_dev_state_change,
|
||||
STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides functionality to keep track of devices.
|
||||
Provide functionality to keep track of devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker/
|
||||
"""
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
# pylint: disable=too-many-locals
|
||||
import csv
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
@@ -25,7 +22,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"
|
||||
@@ -36,7 +33,6 @@ ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
CSV_DEVICES = "known_devices.csv"
|
||||
YAML_DEVICES = 'known_devices.yaml'
|
||||
|
||||
CONF_TRACK_NEW = "track_new_devices"
|
||||
@@ -72,7 +68,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Returns if any or specified device is home. """
|
||||
"""Return the state if any or a specified device is home."""
|
||||
entity = entity_id or ENTITY_ID_ALL_DEVICES
|
||||
|
||||
return hass.states.is_state(entity, STATE_HOME)
|
||||
@@ -80,23 +76,21 @@ def is_on(hass, entity_id=None):
|
||||
|
||||
def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
|
||||
gps=None, gps_accuracy=None, battery=None):
|
||||
""" Call service to notify you see device. """
|
||||
"""Call service to notify you see device."""
|
||||
data = {key: value for key, value in
|
||||
((ATTR_MAC, mac),
|
||||
(ATTR_DEV_ID, dev_id),
|
||||
(ATTR_HOST_NAME, host_name),
|
||||
(ATTR_LOCATION_NAME, location_name),
|
||||
(ATTR_GPS, gps)) if value is not None}
|
||||
(ATTR_GPS, gps),
|
||||
(ATTR_GPS_ACCURACY, gps_accuracy),
|
||||
(ATTR_BATTERY, battery)) if value is not None}
|
||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup device tracker """
|
||||
"""Setup device tracker."""
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
csv_path = hass.config.path(CSV_DEVICES)
|
||||
if os.path.isfile(csv_path) and not os.path.isfile(yaml_path) and \
|
||||
convert_csv_config(csv_path, yaml_path):
|
||||
os.remove(csv_path)
|
||||
|
||||
conf = config.get(DOMAIN, {})
|
||||
if isinstance(conf, list):
|
||||
@@ -114,7 +108,7 @@ def setup(hass, config):
|
||||
devices)
|
||||
|
||||
def setup_platform(p_type, p_config, disc_info=None):
|
||||
""" Setup a device tracker platform. """
|
||||
"""Setup a device tracker platform."""
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
|
||||
if platform is None:
|
||||
return
|
||||
@@ -140,21 +134,21 @@ def setup(hass, config):
|
||||
setup_platform(p_type, p_config)
|
||||
|
||||
def device_tracker_discovered(service, info):
|
||||
""" Called when a device tracker platform is discovered. """
|
||||
"""Called when a device tracker platform is discovered."""
|
||||
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
|
||||
|
||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
|
||||
device_tracker_discovered)
|
||||
|
||||
def update_stale(now):
|
||||
""" Clean up stale devices. """
|
||||
"""Clean up stale devices."""
|
||||
tracker.update_stale(now)
|
||||
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
|
||||
|
||||
tracker.setup_group()
|
||||
|
||||
def see_service(call):
|
||||
""" Service to see a device. """
|
||||
"""Service to see a device."""
|
||||
args = {key: value for key, value in call.data.items() if key in
|
||||
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
|
||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)}
|
||||
@@ -169,8 +163,10 @@ def setup(hass, config):
|
||||
|
||||
|
||||
class DeviceTracker(object):
|
||||
""" Track devices """
|
||||
"""Representation of a device tracker."""
|
||||
|
||||
def __init__(self, hass, consider_home, track_new, home_range, devices):
|
||||
"""Initialize a device tracker."""
|
||||
self.hass = hass
|
||||
self.devices = {dev.dev_id: dev for dev in devices}
|
||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||
@@ -187,7 +183,7 @@ class DeviceTracker(object):
|
||||
|
||||
def see(self, mac=None, dev_id=None, host_name=None, location_name=None,
|
||||
gps=None, gps_accuracy=None, battery=None):
|
||||
""" Notify device tracker that you see a device. """
|
||||
"""Notify the device tracker that you see a device."""
|
||||
with self.lock:
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
@@ -226,14 +222,14 @@ class DeviceTracker(object):
|
||||
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
|
||||
|
||||
def setup_group(self):
|
||||
""" Initializes group for all tracked devices. """
|
||||
"""Initialize group for all tracked devices."""
|
||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||
if dev.track)
|
||||
self.group = group.setup_group(
|
||||
self.group = group.Group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
|
||||
def update_stale(self, now):
|
||||
""" Update stale devices. """
|
||||
"""Update stale devices."""
|
||||
with self.lock:
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home and
|
||||
@@ -242,7 +238,7 @@ class DeviceTracker(object):
|
||||
|
||||
|
||||
class Device(Entity):
|
||||
""" Tracked device. """
|
||||
"""Represent a tracked device."""
|
||||
|
||||
host_name = None
|
||||
location_name = None
|
||||
@@ -251,12 +247,13 @@ class Device(Entity):
|
||||
last_seen = None
|
||||
battery = None
|
||||
|
||||
# Track if the last update of this device was HOME
|
||||
# Track if the last update of this device was HOME.
|
||||
last_update_home = False
|
||||
_state = STATE_NOT_HOME
|
||||
|
||||
def __init__(self, hass, consider_home, home_range, track, dev_id, mac,
|
||||
name=None, picture=None, away_hide=False):
|
||||
"""Initialize a device."""
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
|
||||
@@ -282,28 +279,30 @@ class Device(Entity):
|
||||
|
||||
@property
|
||||
def gps_home(self):
|
||||
""" Return if device is within range of home. """
|
||||
"""Return if device is within range of home."""
|
||||
distance = max(
|
||||
0, self.hass.config.distance(*self.gps) - self.gps_accuracy)
|
||||
return self.gps is not None and distance <= self.home_range
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the entity. """
|
||||
"""Return the name of the entity."""
|
||||
return self.config_name or self.host_name or DEVICE_DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" State of the device. """
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Device state attributes. """
|
||||
attr = {}
|
||||
def entity_picture(self):
|
||||
"""Return the picture of the device."""
|
||||
return self.config_picture
|
||||
|
||||
if self.config_picture:
|
||||
attr[ATTR_ENTITY_PICTURE] = self.config_picture
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attr = {}
|
||||
|
||||
if self.gps:
|
||||
attr[ATTR_LATITUDE] = self.gps[0]
|
||||
@@ -317,12 +316,12 @@ class Device(Entity):
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
""" If device should be hidden. """
|
||||
"""If device should be hidden."""
|
||||
return self.away_hide and self.state != STATE_HOME
|
||||
|
||||
def seen(self, host_name=None, location_name=None, gps=None,
|
||||
gps_accuracy=0, battery=None):
|
||||
""" Mark the device as seen. """
|
||||
"""Mark the device as seen."""
|
||||
self.last_seen = dt_util.utcnow()
|
||||
self.host_name = host_name
|
||||
self.location_name = location_name
|
||||
@@ -340,12 +339,12 @@ class Device(Entity):
|
||||
self.update()
|
||||
|
||||
def stale(self, now=None):
|
||||
""" Return if device state is stale. """
|
||||
"""Return if device state is stale."""
|
||||
return self.last_seen and \
|
||||
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
|
||||
|
||||
def update(self):
|
||||
""" Update state of entity. """
|
||||
"""Update state of entity."""
|
||||
if not self.last_seen:
|
||||
return
|
||||
elif self.location_name:
|
||||
@@ -368,23 +367,8 @@ class Device(Entity):
|
||||
self.last_update_home = True
|
||||
|
||||
|
||||
def convert_csv_config(csv_path, yaml_path):
|
||||
""" Convert CSV config file format to YAML. """
|
||||
used_ids = set()
|
||||
with open(csv_path) as inp:
|
||||
for row in csv.DictReader(inp):
|
||||
dev_id = util.ensure_unique_string(
|
||||
(util.slugify(row['name']) or DEVICE_DEFAULT_NAME).lower(),
|
||||
used_ids)
|
||||
used_ids.add(dev_id)
|
||||
device = Device(None, None, None, row['track'] == '1', dev_id,
|
||||
row['device'], row['name'], row['picture'])
|
||||
update_config(yaml_path, dev_id, device)
|
||||
return True
|
||||
|
||||
|
||||
def load_config(path, hass, consider_home, home_range):
|
||||
""" Load devices from YAML config file. """
|
||||
"""Load devices from YAML configuration file."""
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
return [
|
||||
@@ -396,7 +380,7 @@ def load_config(path, hass, consider_home, home_range):
|
||||
|
||||
|
||||
def setup_scanner_platform(hass, config, scanner, see_device):
|
||||
""" Helper method to connect scanner-based platform to device tracker. """
|
||||
"""Helper method to connect scanner-based platform to device tracker."""
|
||||
interval = util.convert(config.get(CONF_SCAN_INTERVAL), int,
|
||||
DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
@@ -404,7 +388,7 @@ def setup_scanner_platform(hass, config, scanner, see_device):
|
||||
seen = set()
|
||||
|
||||
def device_tracker_scan(now):
|
||||
""" Called when interval matches. """
|
||||
"""Called when interval matches."""
|
||||
for mac in scanner.scan_devices():
|
||||
if mac in seen:
|
||||
host_name = None
|
||||
@@ -420,7 +404,7 @@ def setup_scanner_platform(hass, config, scanner, see_device):
|
||||
|
||||
|
||||
def update_config(path, dev_id, device):
|
||||
""" Add device to YAML config file. """
|
||||
"""Add device to YAML configuration file."""
|
||||
with open(path, 'a') as out:
|
||||
out.write('\n')
|
||||
out.write('{}:\n'.format(device.dev_id))
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.actiontec
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning an Actiontec MI424WR
|
||||
(Verizon FIOS) router for device presence.
|
||||
Support for Actiontec MI424WR (Verizon FIOS) routers.
|
||||
|
||||
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
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -34,7 +31,7 @@ _LEASES_REGEX = re.compile(
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns an Actiontec scanner. """
|
||||
"""Validate the configuration and return an Actiontec scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
@@ -46,12 +43,10 @@ Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||
|
||||
|
||||
class ActiontecDeviceScanner(object):
|
||||
"""
|
||||
This class queries a an actiontec router for connected devices.
|
||||
Adapted from DD-WRT scanner.
|
||||
"""
|
||||
"""This class queries a an actiontec router for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
@@ -62,15 +57,12 @@ class ActiontecDeviceScanner(object):
|
||||
_LOGGER.info("actiontec scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device ids.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client.mac for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
@@ -80,9 +72,9 @@ class ActiontecDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the Actiontec MI424WR router is up
|
||||
to date. Returns boolean if scanning successful.
|
||||
"""Ensure the information from the router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info("Scanning")
|
||||
if not self.success_init:
|
||||
@@ -100,7 +92,7 @@ class ActiontecDeviceScanner(object):
|
||||
return True
|
||||
|
||||
def get_actiontec_data(self):
|
||||
""" Retrieve data from Actiontec MI424WR and return parsed result. """
|
||||
"""Retrieve data from Actiontec MI424WR and return parsed result."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'Username: ')
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.aruba
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a Aruba Access Point for device
|
||||
presence.
|
||||
Support for Aruba Access Points.
|
||||
|
||||
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(
|
||||
@@ -31,7 +28,7 @@ _DEVICES_REGEX = re.compile(
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Aruba scanner. """
|
||||
"""Validate the configuration and return a Aruba scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
@@ -43,8 +40,10 @@ def get_scanner(hass, config):
|
||||
|
||||
|
||||
class ArubaDeviceScanner(object):
|
||||
""" This class queries a Aruba Acces Point for connected devices. """
|
||||
"""This class queries a Aruba Access Point for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
@@ -53,20 +52,17 @@ class ArubaDeviceScanner(object):
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
# Test the router is accessible
|
||||
# Test the router is accessible.
|
||||
data = self.get_aruba_data()
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device IDs.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client['mac'] for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
@@ -76,9 +72,9 @@ class ArubaDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the Aruba Access Point is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""Ensure the information from the Aruba Access Point is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
@@ -92,24 +88,39 @@ class ArubaDeviceScanner(object):
|
||||
return True
|
||||
|
||||
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")
|
||||
"""Retrieve data from Aruba Access Point and return parsed result."""
|
||||
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 +130,5 @@ class ArubaDeviceScanner(object):
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
'name': match.group('name')
|
||||
}
|
||||
}
|
||||
return devices
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.asuswrt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a ASUSWRT router for device
|
||||
presence.
|
||||
Support for ASUSWRT routers.
|
||||
|
||||
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
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -39,7 +36,7 @@ _IP_NEIGH_REGEX = re.compile(
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns an ASUS-WRT scanner. """
|
||||
"""Validate the configuration and return an ASUS-WRT scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
@@ -51,34 +48,29 @@ def get_scanner(hass, config):
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(object):
|
||||
"""
|
||||
This class queries a router running ASUSWRT firmware
|
||||
for connected devices. Adapted from DD-WRT scanner.
|
||||
"""
|
||||
"""This class queries a router running ASUSWRT firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.username = str(config[CONF_USERNAME])
|
||||
self.password = str(config[CONF_PASSWORD])
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
# Test the router is accessible
|
||||
# Test the router is accessible.
|
||||
data = self.get_asuswrt_data()
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device IDs.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client['mac'] for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
@@ -88,9 +80,9 @@ class AsusWrtDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the ASUSWRT router is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""Ensure the information from the ASUSWRT router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
@@ -109,7 +101,7 @@ class AsusWrtDeviceScanner(object):
|
||||
return True
|
||||
|
||||
def get_asuswrt_data(self):
|
||||
""" Retrieve data from ASUSWRT and return parsed result. """
|
||||
"""Retrieve data from ASUSWRT and return parsed result."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'login: ')
|
||||
@@ -138,9 +130,8 @@ class AsusWrtDeviceScanner(object):
|
||||
_LOGGER.warning("Could not parse lease row: %s", lease)
|
||||
continue
|
||||
|
||||
# For leases where the client doesn't set a hostname, ensure
|
||||
# it is blank and not '*', which breaks the entity_id down
|
||||
# the line
|
||||
# For leases where the client doesn't set a hostname, ensure it is
|
||||
# blank and not '*', which breaks the entity_id down the line.
|
||||
host = match.group('host')
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.ddwrt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a DD-WRT router for device
|
||||
presence.
|
||||
Support for DD-WRT routers.
|
||||
|
||||
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
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -29,7 +27,7 @@ _MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a DD-WRT scanner. """
|
||||
"""Validate the configuration and return a DD-WRT scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
@@ -42,12 +40,10 @@ def get_scanner(hass, config):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class DdWrtDeviceScanner(object):
|
||||
"""
|
||||
This class queries a wireless router running DD-WRT firmware
|
||||
for connected devices. Adapted from Tomato scanner.
|
||||
"""
|
||||
"""This class queries a wireless router running DD-WRT firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
@@ -64,19 +60,15 @@ class DdWrtDeviceScanner(object):
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device ids.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return self.last_results
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
# if not initialised and not already scanned and not found
|
||||
# If not initialised and not already scanned and not found.
|
||||
if device not in self.mac2name:
|
||||
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
@@ -89,15 +81,15 @@ class DdWrtDeviceScanner(object):
|
||||
if not dhcp_leases:
|
||||
return None
|
||||
|
||||
# remove leading and trailing single quotes
|
||||
# Remove leading and trailing single quotes.
|
||||
cleaned_str = dhcp_leases.strip().strip('"')
|
||||
elements = cleaned_str.split('","')
|
||||
num_clients = int(len(elements)/5)
|
||||
self.mac2name = {}
|
||||
for idx in range(0, num_clients):
|
||||
# this is stupid but the data is a single array
|
||||
# This is stupid but the data is a single array
|
||||
# every 5 elements represents one hosts, the MAC
|
||||
# is the third element and the name is the first
|
||||
# is the third element and the name is the first.
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
mac = elements[mac_index]
|
||||
@@ -107,9 +99,9 @@ class DdWrtDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the DD-WRT router is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""Ensure the information from the DD-WRT router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
@@ -134,7 +126,7 @@ class DdWrtDeviceScanner(object):
|
||||
# regex's out values so I guess I have to do the same,
|
||||
# LAME!!!
|
||||
|
||||
# remove leading and trailing single quotes
|
||||
# Remove leading and trailing single quotes.
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
|
||||
@@ -144,7 +136,7 @@ class DdWrtDeviceScanner(object):
|
||||
return True
|
||||
|
||||
def get_ddwrt_data(self, url):
|
||||
""" Retrieve data from DD-WRT and return parsed result. """
|
||||
"""Retrieve data from DD-WRT and return parsed result."""
|
||||
try:
|
||||
response = requests.get(
|
||||
url,
|
||||
@@ -166,7 +158,7 @@ class DdWrtDeviceScanner(object):
|
||||
|
||||
|
||||
def _parse_ddwrt_response(data_str):
|
||||
""" Parse the DD-WRT data format. """
|
||||
"""Parse the DD-WRT data format."""
|
||||
return {
|
||||
key: val for key, val in _DDWRT_DATA_REGEX
|
||||
.findall(data_str)}
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Demo platform for the device tracker.
|
||||
|
||||
device_tracker:
|
||||
platform: demo
|
||||
"""
|
||||
"""Demo platform for the device tracker."""
|
||||
import random
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a demo tracker. """
|
||||
|
||||
"""Setup the demo tracker."""
|
||||
def offset():
|
||||
""" Return random offset. """
|
||||
"""Return random offset."""
|
||||
return (random.randrange(500, 2000)) / 2e5 * random.choice((-1, 1))
|
||||
|
||||
def random_see(dev_id, name):
|
||||
""" Randomize a sighting. """
|
||||
"""Randomize a sighting."""
|
||||
see(
|
||||
dev_id=dev_id,
|
||||
host_name=name,
|
||||
@@ -30,7 +22,7 @@ def setup_scanner(hass, config, see):
|
||||
)
|
||||
|
||||
def observe(call=None):
|
||||
""" Observe three entities. """
|
||||
"""Observe three entities."""
|
||||
random_see('demo_paulus', 'Paulus')
|
||||
random_see('demo_anne_therese', 'Anne Therese')
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Support for FRITZ!Box routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.fritz/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
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
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.4.6']
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return FritzBoxScanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: []},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = FritzBoxScanner(config[DOMAIN])
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class FritzBoxScanner(object):
|
||||
"""This class queries a FRITZ!Box router."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.last_results = []
|
||||
self.host = '169.254.1.1' # This IP is valid for all FRITZ!Box router.
|
||||
self.username = 'admin'
|
||||
self.password = ''
|
||||
self.success_init = True
|
||||
|
||||
# pylint: disable=import-error
|
||||
import fritzconnection as fc
|
||||
|
||||
# Check for user specific configuration
|
||||
if CONF_HOST in config.keys():
|
||||
self.host = config[CONF_HOST]
|
||||
if CONF_USERNAME in config.keys():
|
||||
self.username = config[CONF_USERNAME]
|
||||
if CONF_PASSWORD in config.keys():
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
# Establish a connection to the FRITZ!Box.
|
||||
try:
|
||||
self.fritz_box = fc.FritzHosts(address=self.host,
|
||||
user=self.username,
|
||||
password=self.password)
|
||||
except (ValueError, TypeError):
|
||||
self.fritz_box = None
|
||||
|
||||
# At this point it is difficult to tell if a connection is established.
|
||||
# So just check for null objects.
|
||||
if self.fritz_box is None or not self.fritz_box.modelname:
|
||||
self.success_init = False
|
||||
|
||||
if self.success_init:
|
||||
_LOGGER.info("Successfully connected to %s",
|
||||
self.fritz_box.modelname)
|
||||
self._update_info()
|
||||
else:
|
||||
_LOGGER.error("Failed to establish connection to FRITZ!Box "
|
||||
"with IP: %s", self.host)
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list of found device ids."""
|
||||
self._update_info()
|
||||
active_hosts = []
|
||||
for known_host in self.last_results:
|
||||
if known_host["status"] == "1":
|
||||
active_hosts.append(known_host["mac"])
|
||||
return active_hosts
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name of the given device or None if is not known."""
|
||||
ret = self.fritz_box.get_specific_host_entry(mac)["NewHostName"]
|
||||
if ret == {}:
|
||||
return None
|
||||
return ret
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Retrieve latest information from the FRITZ!Box."""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
_LOGGER.info("Scanning")
|
||||
self.last_results = self.fritz_box.get_hosts_info()
|
||||
return True
|
||||
@@ -1,70 +0,0 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.geofancy
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Geofancy platform for the device tracker.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.geofancy/
|
||||
"""
|
||||
from homeassistant.const import (
|
||||
HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_SEE = 0
|
||||
|
||||
URL_API_GEOFANCY_ENDPOINT = "/api/geofancy"
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up an endpoint for the Geofancy app. """
|
||||
|
||||
# Use a global variable to keep setup_scanner compact when using a callback
|
||||
global _SEE
|
||||
_SEE = see
|
||||
|
||||
# POST would be semantically better, but that currently does not work
|
||||
# since Geofancy sends the data as key1=value1&key2=value2
|
||||
# in the request body, while Home Assistant expects json there.
|
||||
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_GEOFANCY_ENDPOINT, _handle_get_api_geofancy)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_geofancy(handler, path_match, data):
|
||||
""" Geofancy message received. """
|
||||
|
||||
if not isinstance(data, dict):
|
||||
handler.write_json_message(
|
||||
"Error while parsing Geofancy message.",
|
||||
HTTP_INTERNAL_SERVER_ERROR)
|
||||
return
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
handler.write_json_message(
|
||||
"Location not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
if 'device' not in data or 'id' not in data:
|
||||
handler.write_json_message(
|
||||
"Device id or location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
try:
|
||||
gps_coords = (float(data['latitude']), float(data['longitude']))
|
||||
except ValueError:
|
||||
# If invalid latitude / longitude format
|
||||
handler.write_json_message(
|
||||
"Invalid latitude / longitude format.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
# entity id's in Home Assistant must be alphanumerical
|
||||
device_uuid = data['device']
|
||||
device_entity_id = device_uuid.replace('-', '')
|
||||
|
||||
_SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id'])
|
||||
|
||||
handler.write_json_message("Geofancy message processed")
|
||||
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Support for iCloud connected devices.
|
||||
|
||||
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_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyicloud==0.7.2']
|
||||
|
||||
CONF_INTERVAL = 'interval'
|
||||
DEFAULT_INTERVAL = 8
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup the iCloud Scanner."""
|
||||
from pyicloud import PyiCloudService
|
||||
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
|
||||
# Get the username and password from the configuration.
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error('Must specify a username and password')
|
||||
return False
|
||||
|
||||
try:
|
||||
_LOGGER.info('Logging into iCloud Account')
|
||||
# Attempt the login to iCloud
|
||||
api = PyiCloudService(username,
|
||||
password,
|
||||
verify=True)
|
||||
except PyiCloudFailedLoginException as error:
|
||||
_LOGGER.exception('Error logging into iCloud Service: %s', error)
|
||||
return False
|
||||
|
||||
def keep_alive(now):
|
||||
"""Keep authenticating iCloud connection."""
|
||||
api.authenticate()
|
||||
_LOGGER.info("Authenticate against iCloud")
|
||||
|
||||
track_utc_time_change(hass, keep_alive, second=0)
|
||||
|
||||
def update_icloud(now):
|
||||
"""Authenticate against iCloud and scan for devices."""
|
||||
try:
|
||||
# The session timeouts if we are not using it so we
|
||||
# have to re-authenticate. This will send an email.
|
||||
api.authenticate()
|
||||
# Loop through every device registered with the iCloud account
|
||||
for device in api.devices:
|
||||
status = device.status()
|
||||
location = device.location()
|
||||
# If the device has a location add it. If not do nothing
|
||||
if location:
|
||||
see(
|
||||
dev_id=re.sub(r"(\s|\W|')",
|
||||
'',
|
||||
status['name']),
|
||||
host_name=status['name'],
|
||||
gps=(location['latitude'], location['longitude']),
|
||||
battery=status['batteryLevel']*100,
|
||||
gps_accuracy=location['horizontalAccuracy']
|
||||
)
|
||||
else:
|
||||
# No location found for the device so continue
|
||||
continue
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.info('No iCloud Devices found!')
|
||||
|
||||
track_utc_time_change(
|
||||
hass, update_icloud,
|
||||
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)),
|
||||
second=0
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Support for the Locative platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.locative/
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup an endpoint for the Locative application."""
|
||||
# POST would be semantically better, but that currently does not work
|
||||
# since Locative sends the data as key1=value1&key2=value2
|
||||
# in the request body, while Home Assistant expects json there.
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_LOCATIVE_ENDPOINT,
|
||||
partial(_handle_get_api_locative, hass, see))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_locative(hass, see, handler, path_match, data):
|
||||
"""Locative message received."""
|
||||
if not _check_data(handler, data):
|
||||
return
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
|
||||
if direction == 'enter':
|
||||
see(dev_id=device, location_name=location_name)
|
||||
handler.write_text("Setting location to {}".format(location_name))
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = hass.states.get("{}.{}".format(DOMAIN, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
see(dev_id=device, location_name=STATE_NOT_HOME)
|
||||
handler.write_text("Setting location to not home")
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered before
|
||||
# the previous zone was exited. The enter message will be sent
|
||||
# first, then the exit message will be sent second.
|
||||
handler.write_text(
|
||||
'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state))
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
handler.write_text("Received test message.")
|
||||
|
||||
else:
|
||||
handler.write_text(
|
||||
"Received unidentified message: {}".format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Received unidentified message from Locative: %s",
|
||||
direction)
|
||||
|
||||
|
||||
def _check_data(handler, data):
|
||||
"""Check the data."""
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
handler.write_text("Latitude and longitude not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Latitude and longitude not specified.")
|
||||
return False
|
||||
|
||||
if 'device' not in data:
|
||||
handler.write_text("Device id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Device id not specified.")
|
||||
return False
|
||||
|
||||
if 'id' not in data:
|
||||
handler.write_text("Location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Location id not specified.")
|
||||
return False
|
||||
|
||||
if 'trigger' not in data:
|
||||
handler.write_text("Trigger is not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Trigger is not specified.")
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,32 +1,30 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.luci
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a OpenWRT router for device
|
||||
presence.
|
||||
Support for OpenWRT (luci) routers.
|
||||
|
||||
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
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Luci scanner. """
|
||||
"""Validate the configuration and return a Luci scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
@@ -39,20 +37,13 @@ def get_scanner(hass, config):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class LuciDeviceScanner(object):
|
||||
"""
|
||||
This class queries a wireless router running OpenWrt firmware
|
||||
for connected devices. Adapted from Tomato scanner.
|
||||
"""This class queries a wireless router running OpenWrt firmware.
|
||||
|
||||
# opkg install luci-mod-rpc
|
||||
for this to work on the router.
|
||||
|
||||
The API is described here:
|
||||
http://luci.subsignal.org/trac/wiki/Documentation/JsonRpcHowTo
|
||||
|
||||
(Currently, we do only wifi iwscan, and no DHCP lease access.)
|
||||
Adapted from Tomato scanner.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
|
||||
@@ -69,17 +60,12 @@ class LuciDeviceScanner(object):
|
||||
self.success_init = self.token is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device ids.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return self.last_results
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
if self.mac2name is None:
|
||||
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
|
||||
@@ -99,8 +85,8 @@ class LuciDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the Luci router is up to date.
|
||||
"""Ensure the information from the Luci router is up to date.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
@@ -126,7 +112,7 @@ class LuciDeviceScanner(object):
|
||||
|
||||
|
||||
def _req_json_rpc(url, method, *args, **kwargs):
|
||||
""" Perform one JSON RPC operation. """
|
||||
"""Perform one JSON RPC operation."""
|
||||
data = json.dumps({'method': method, 'params': args})
|
||||
try:
|
||||
res = requests.post(url, data=data, timeout=5, **kwargs)
|
||||
@@ -156,6 +142,6 @@ def _req_json_rpc(url, method, *args, **kwargs):
|
||||
|
||||
|
||||
def _get_token(host, username, password):
|
||||
""" Get authentication token for the given host+username+password. """
|
||||
"""Get authentication token for the given host+username+password."""
|
||||
url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host)
|
||||
return _req_json_rpc(url, 'login', username, password)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
MQTT platform for the device tracker.
|
||||
Support for tracking MQTT enabled devices.
|
||||
|
||||
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']
|
||||
|
||||
@@ -21,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a MQTT tracker. """
|
||||
"""Setup the MQTT tracker."""
|
||||
devices = config.get(CONF_DEVICES)
|
||||
qos = util.convert(config.get(CONF_QOS), int, DEFAULT_QOS)
|
||||
|
||||
@@ -33,7 +32,7 @@ def setup_scanner(hass, config, see):
|
||||
dev_id_lookup = {}
|
||||
|
||||
def device_tracker_message_received(topic, payload, qos):
|
||||
""" MQTT message received. """
|
||||
"""MQTT message received."""
|
||||
see(dev_id=dev_id_lookup[topic], location_name=payload)
|
||||
|
||||
for dev_id, topic in devices.items():
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.netgear
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a Netgear router for device
|
||||
presence.
|
||||
Support for Netgear routers.
|
||||
|
||||
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
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pynetgear==0.3']
|
||||
REQUIREMENTS = ['pynetgear==0.3.2']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Netgear scanner. """
|
||||
"""Validate the configuration and returns a Netgear scanner."""
|
||||
info = config[DOMAIN]
|
||||
host = info.get(CONF_HOST)
|
||||
username = info.get(CONF_USERNAME)
|
||||
@@ -39,9 +36,10 @@ def get_scanner(hass, config):
|
||||
|
||||
|
||||
class NetgearDeviceScanner(object):
|
||||
""" This class queries a Netgear wireless router using the SOAP-API. """
|
||||
"""Queries a Netgear wireless router using the SOAP-API."""
|
||||
|
||||
def __init__(self, host, username, password):
|
||||
"""Initialize the scanner."""
|
||||
import pynetgear
|
||||
|
||||
self.last_results = []
|
||||
@@ -66,15 +64,13 @@ class NetgearDeviceScanner(object):
|
||||
_LOGGER.error("Failed to Login")
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device ids.
|
||||
"""
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return (device.mac for device in self.last_results)
|
||||
|
||||
def get_device_name(self, mac):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
try:
|
||||
return next(device.name for device in self.last_results
|
||||
if device.mac == mac)
|
||||
@@ -83,8 +79,8 @@ class NetgearDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Retrieves latest information from the Netgear router.
|
||||
"""Retrieve latest information from the Netgear router.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.nmap
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a network with nmap.
|
||||
Support for scanning a network with nmap.
|
||||
|
||||
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)
|
||||
@@ -30,7 +28,7 @@ REQUIREMENTS = ['python-nmap==0.4.3']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Nmap scanner. """
|
||||
"""Validate the configuration and return a Nmap scanner."""
|
||||
if not validate_config(config, {DOMAIN: [CONF_HOSTS]},
|
||||
_LOGGER):
|
||||
return None
|
||||
@@ -43,7 +41,7 @@ Device = namedtuple("Device", ["mac", "name", "ip", "last_update"])
|
||||
|
||||
|
||||
def _arp(ip_address):
|
||||
""" Get the MAC address for a given IP. """
|
||||
"""Get the MAC address for a given IP."""
|
||||
cmd = ['arp', '-n', ip_address]
|
||||
arp = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
out, _ = arp.communicate()
|
||||
@@ -55,9 +53,10 @@ def _arp(ip_address):
|
||||
|
||||
|
||||
class NmapDeviceScanner(object):
|
||||
""" This class scans for devices using nmap. """
|
||||
"""This class scans for devices using nmap."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.last_results = []
|
||||
|
||||
self.hosts = config[CONF_HOSTS]
|
||||
@@ -68,17 +67,13 @@ class NmapDeviceScanner(object):
|
||||
_LOGGER.info("nmap scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device ids.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, mac):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
filter_named = [device.name for device in self.last_results
|
||||
if device.mac == mac]
|
||||
|
||||
@@ -89,8 +84,8 @@ class NmapDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Scans the network for devices.
|
||||
"""Scan the network for devices.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info("Scanning")
|
||||
@@ -98,7 +93,7 @@ class NmapDeviceScanner(object):
|
||||
from nmap import PortScanner, PortScannerError
|
||||
scanner = PortScanner()
|
||||
|
||||
options = "-F --host-timeout 5"
|
||||
options = "-F --host-timeout 5s"
|
||||
|
||||
if self.home_interval:
|
||||
boundary = dt_util.now() - self.home_interval
|
||||
|
||||
@@ -1,53 +1,194 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.owntracks
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
OwnTracks platform for the device tracker.
|
||||
Support the OwnTracks platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.owntracks/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import STATE_HOME
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
REGIONS_ENTERED = defaultdict(list)
|
||||
MOBILE_BEACONS_ACTIVE = defaultdict(list)
|
||||
|
||||
BEACON_DEV_ID = 'beacon'
|
||||
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LOCK = threading.Lock()
|
||||
|
||||
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a OwnTracksks tracker. """
|
||||
"""Setup an OwnTracks tracker."""
|
||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||
|
||||
def owntracks_location_update(topic, payload, qos):
|
||||
""" MQTT message received. """
|
||||
|
||||
"""MQTT message received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
logging.getLogger(__name__).error(
|
||||
_LOGGER.error(
|
||||
'Unable to parse payload as JSON: %s', payload)
|
||||
return
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != 'location':
|
||||
if (not isinstance(data, dict) or data.get('_type') != 'location') or (
|
||||
'acc' in data and max_gps_accuracy is not None and data[
|
||||
'acc'] > max_gps_accuracy):
|
||||
return
|
||||
|
||||
parts = topic.split('/')
|
||||
kwargs = {
|
||||
'dev_id': '{}_{}'.format(parts[1], parts[2]),
|
||||
'host_name': parts[1],
|
||||
'gps': (data['lat'], data['lon']),
|
||||
}
|
||||
if 'acc' in data:
|
||||
kwargs['gps_accuracy'] = data['acc']
|
||||
if 'batt' in data:
|
||||
kwargs['battery'] = data['batt']
|
||||
dev_id, kwargs = _parse_see_args(topic, data)
|
||||
|
||||
see(**kwargs)
|
||||
# Block updates if we're in a region
|
||||
with LOCK:
|
||||
if REGIONS_ENTERED[dev_id]:
|
||||
_LOGGER.debug(
|
||||
"location update ignored - inside region %s",
|
||||
REGIONS_ENTERED[-1])
|
||||
return
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
|
||||
def owntracks_event_update(topic, payload, qos):
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
"""MQTT event (geofences) received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
_LOGGER.error(
|
||||
'Unable to parse payload as JSON: %s', payload)
|
||||
return
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != 'transition':
|
||||
return
|
||||
|
||||
# OwnTracks uses - at the start of a beacon zone
|
||||
# to switch on 'hold mode' - ignore this
|
||||
location = data['desc'].lstrip("-")
|
||||
if location.lower() == 'home':
|
||||
location = STATE_HOME
|
||||
|
||||
dev_id, kwargs = _parse_see_args(topic, data)
|
||||
|
||||
if data['event'] == 'enter':
|
||||
zone = hass.states.get("zone.{}".format(location))
|
||||
with LOCK:
|
||||
if zone is None:
|
||||
if data['t'] == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location not in beacons:
|
||||
beacons.append(location)
|
||||
_LOGGER.info("Added beacon %s", location)
|
||||
else:
|
||||
# Normal region
|
||||
if not zone.attributes.get('passive'):
|
||||
kwargs['location_name'] = location
|
||||
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location not in regions:
|
||||
regions.append(location)
|
||||
_LOGGER.info("Enter region %s", location)
|
||||
_set_gps_from_zone(kwargs, zone)
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
|
||||
elif data['event'] == 'leave':
|
||||
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))
|
||||
if not zone.attributes.get('passive'):
|
||||
kwargs['location_name'] = new_region
|
||||
_set_gps_from_zone(kwargs, zone)
|
||||
_LOGGER.info("Exit to %s", new_region)
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
|
||||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
# Check for GPS accuracy
|
||||
if not ('acc' in data and
|
||||
max_gps_accuracy is not None and
|
||||
data['acc'] > max_gps_accuracy):
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
else:
|
||||
_LOGGER.info("Inaccurate GPS reported")
|
||||
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
_LOGGER.info("Remove beacon %s", location)
|
||||
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Misformatted mqtt msgs, _type=transition, event=%s',
|
||||
data['event'])
|
||||
return
|
||||
|
||||
def see_beacons(dev_id, kwargs_param):
|
||||
"""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
|
||||
see(**kwargs)
|
||||
|
||||
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
||||
mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _parse_see_args(topic, data):
|
||||
"""Parse the OwnTracks location parameters, into the format see expects."""
|
||||
parts = topic.split('/')
|
||||
dev_id = '{}_{}'.format(parts[1], parts[2])
|
||||
host_name = parts[1]
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
'host_name': host_name,
|
||||
'gps': (data['lat'], data['lon'])
|
||||
}
|
||||
if 'acc' in data:
|
||||
kwargs['gps_accuracy'] = data['acc']
|
||||
if 'batt' in data:
|
||||
kwargs['battery'] = data['batt']
|
||||
return dev_id, kwargs
|
||||
|
||||
|
||||
def _set_gps_from_zone(kwargs, zone):
|
||||
"""Set the see parameters from the zone parameters."""
|
||||
if zone is not None:
|
||||
kwargs['gps'] = (
|
||||
zone.attributes['latitude'],
|
||||
zone.attributes['longitude'])
|
||||
kwargs['gps_accuracy'] = zone.attributes['radius']
|
||||
return kwargs
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Describes the format for available device tracker services
|
||||
|
||||
see:
|
||||
description: Control tracked device
|
||||
|
||||
fields:
|
||||
mac:
|
||||
description: MAC address of device
|
||||
example: 'FF:FF:FF:FF:FF:FF'
|
||||
|
||||
dev_id:
|
||||
description: Id of device (find id in known_devices.yaml)
|
||||
example: 'phonedave'
|
||||
|
||||
host_name:
|
||||
description: Hostname of device
|
||||
example: 'Dave'
|
||||
|
||||
location_name:
|
||||
description: Name of location where device is located (not_home is away)
|
||||
example: 'home'
|
||||
|
||||
gps:
|
||||
description: GPS coordinates where device is located (latitude, longitude)
|
||||
example: '[51.509802, -0.086692]'
|
||||
|
||||
gps_accuracy:
|
||||
description: Accuracy of GPS coordinates
|
||||
example: '80'
|
||||
|
||||
battery:
|
||||
description: Battery level of device
|
||||
example: '100'
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.snmp
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports fetching WiFi associations
|
||||
through SNMP.
|
||||
Support for fetching WiFi associations 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
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -29,7 +26,7 @@ CONF_BASEOID = "baseoid"
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns an snmp scanner """
|
||||
"""Validate the configuration and return an snmp scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_COMMUNITY, CONF_BASEOID]},
|
||||
_LOGGER):
|
||||
@@ -41,10 +38,10 @@ def get_scanner(hass, config):
|
||||
|
||||
|
||||
class SnmpScanner(object):
|
||||
"""
|
||||
This class queries any SNMP capable Acces Point for connected devices.
|
||||
"""
|
||||
"""Queries any SNMP capable Access Point for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
self.snmp = cmdgen.CommandGenerator()
|
||||
|
||||
@@ -61,25 +58,23 @@ class SnmpScanner(object):
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device IDs.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client['mac'] for client in self.last_results]
|
||||
return [client['mac'] for client in self.last_results
|
||||
if client.get('mac')]
|
||||
|
||||
# Supressing no-self-use warning
|
||||
# pylint: disable=R0201
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
# We have no names
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the WAP is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""Ensure the information from the WAP is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
@@ -93,8 +88,7 @@ class SnmpScanner(object):
|
||||
return True
|
||||
|
||||
def get_snmp_data(self):
|
||||
""" Fetch mac addresses from WAP via SNMP. """
|
||||
|
||||
"""Fetch MAC addresses from WAP via SNMP."""
|
||||
devices = []
|
||||
|
||||
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
|
||||
@@ -105,13 +99,13 @@ class SnmpScanner(object):
|
||||
return
|
||||
if errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
errindex and restable[-1][int(errindex)-1]
|
||||
or '?')
|
||||
errindex and restable[-1][int(errindex)-1] or '?')
|
||||
return
|
||||
|
||||
for resrow in restable:
|
||||
for _, val in resrow:
|
||||
mac = binascii.hexlify(val.asOctets()).decode('utf-8')
|
||||
_LOGGER.debug('Found mac %s', mac)
|
||||
mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
|
||||
devices.append({'mac': mac})
|
||||
return devices
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.thomson
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a THOMSON router for device
|
||||
presence.
|
||||
Support for THOMSON routers.
|
||||
|
||||
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
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,7 +32,7 @@ _DEVICES_REGEX = re.compile(
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a THOMSON scanner. """
|
||||
"""Validate the configuration and return a THOMSON scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
@@ -47,12 +44,10 @@ def get_scanner(hass, config):
|
||||
|
||||
|
||||
class ThomsonDeviceScanner(object):
|
||||
"""
|
||||
This class queries a router running THOMSON firmware
|
||||
for connected devices. Adapted from ASUSWRT scanner.
|
||||
"""
|
||||
"""This class queries a router running THOMSON firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
@@ -61,20 +56,17 @@ class ThomsonDeviceScanner(object):
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
# Test the router is accessible
|
||||
# Test the router is accessible.
|
||||
data = self.get_thomson_data()
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for new devices and return a
|
||||
list containing found device ids. """
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client['mac'] for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device
|
||||
or None if we don't know. """
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
@@ -84,9 +76,9 @@ class ThomsonDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the THOMSON router is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""Ensure the information from the THOMSON router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
@@ -97,14 +89,14 @@ class ThomsonDeviceScanner(object):
|
||||
if not data:
|
||||
return False
|
||||
|
||||
# flag C stands for CONNECTED
|
||||
# Flag C stands for CONNECTED
|
||||
active_clients = [client for client in data.values() if
|
||||
client['status'].find('C') != -1]
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
def get_thomson_data(self):
|
||||
""" Retrieve data from THOMSON and return parsed result. """
|
||||
"""Retrieve data from THOMSON and return parsed result."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'Username : ')
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.tomato
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a Tomato router for device
|
||||
presence.
|
||||
Support for Tomato routers.
|
||||
|
||||
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
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
CONF_HTTP_ID = "http_id"
|
||||
@@ -29,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Tomato scanner. """
|
||||
"""Validate the configuration and returns a Tomato scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME,
|
||||
CONF_PASSWORD, CONF_HTTP_ID]},
|
||||
@@ -40,14 +37,10 @@ def get_scanner(hass, config):
|
||||
|
||||
|
||||
class TomatoDeviceScanner(object):
|
||||
""" This class queries a wireless router running Tomato firmware
|
||||
for connected devices.
|
||||
|
||||
A description of the Tomato API can be found on
|
||||
http://paulusschoutsen.nl/blog/2013/10/tomato-api-documentation/
|
||||
"""
|
||||
"""This class queries a wireless router running Tomato firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host, http_id = config[CONF_HOST], config[CONF_HTTP_ID]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
|
||||
@@ -68,16 +61,13 @@ class TomatoDeviceScanner(object):
|
||||
self.success_init = self._update_tomato_info()
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for new devices and return a
|
||||
list containing found device ids. """
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_tomato_info()
|
||||
|
||||
return [item[1] for item in self.last_results['wldev']]
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
filter_named = [item[0] for item in self.last_results['dhcpd_lease']
|
||||
if item[2] == device]
|
||||
|
||||
@@ -88,19 +78,17 @@ class TomatoDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_tomato_info(self):
|
||||
""" Ensures the information from the Tomato router is up to date.
|
||||
Returns boolean if scanning successful. """
|
||||
"""Ensure the information from the Tomato router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
with self.lock:
|
||||
self.logger.info("Scanning")
|
||||
|
||||
try:
|
||||
response = requests.Session().send(self.req, timeout=3)
|
||||
|
||||
# Calling and parsing the Tomato api here. We only need the
|
||||
# wldev and dhcpd_lease values. For API description see:
|
||||
# http://paulusschoutsen.nl/
|
||||
# blog/2013/10/tomato-api-documentation/
|
||||
# wldev and dhcpd_lease values.
|
||||
if response.status_code == 200:
|
||||
|
||||
for param, value in \
|
||||
@@ -109,7 +97,6 @@ class TomatoDeviceScanner(object):
|
||||
if param == 'wldev' or param == 'dhcpd_lease':
|
||||
self.last_results[param] = \
|
||||
json.loads(value.replace("'", '"'))
|
||||
|
||||
return True
|
||||
|
||||
elif response.status_code == 401:
|
||||
@@ -117,29 +104,25 @@ class TomatoDeviceScanner(object):
|
||||
self.logger.exception((
|
||||
"Failed to authenticate, "
|
||||
"please check your username and password"))
|
||||
|
||||
return False
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
# We get this if we could not connect to the router or
|
||||
# an invalid http_id was supplied
|
||||
# an invalid http_id was supplied.
|
||||
self.logger.exception((
|
||||
"Failed to connect to the router"
|
||||
" or invalid http_id supplied"))
|
||||
|
||||
return False
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
# We get this if we could not connect to the router or
|
||||
# an invalid http_id was supplied
|
||||
# an invalid http_id was supplied.
|
||||
self.logger.exception(
|
||||
"Connection to the router timed out")
|
||||
|
||||
return False
|
||||
|
||||
except ValueError:
|
||||
# If json decoder could not parse the response
|
||||
# If JSON decoder could not parse the response.
|
||||
self.logger.exception(
|
||||
"Failed to parse response from router")
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.tplink
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a TP-Link router for device
|
||||
presence.
|
||||
Support for TP-Link routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.tplink/
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
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)
|
||||
@@ -26,30 +25,31 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a TP-Link scanner. """
|
||||
"""Validate the configuration and return a TP-Link scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = Tplink3DeviceScanner(config[DOMAIN])
|
||||
scanner = Tplink4DeviceScanner(config[DOMAIN])
|
||||
|
||||
if not scanner.success_init:
|
||||
scanner = Tplink3DeviceScanner(config[DOMAIN])
|
||||
|
||||
if not scanner.success_init:
|
||||
scanner = Tplink2DeviceScanner(config[DOMAIN])
|
||||
|
||||
if not scanner.success_init:
|
||||
scanner = TplinkDeviceScanner(config[DOMAIN])
|
||||
if not scanner.success_init:
|
||||
scanner = TplinkDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class TplinkDeviceScanner(object):
|
||||
"""
|
||||
This class queries a wireless router running TP-Link firmware
|
||||
for connected devices.
|
||||
"""
|
||||
"""This class queries a wireless router running TP-Link firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
|
||||
@@ -65,29 +65,21 @@ class TplinkDeviceScanner(object):
|
||||
self.success_init = self._update_info()
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device ids.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return self.last_results
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""
|
||||
The TP-Link firmware doesn't save the name of the wireless device.
|
||||
"""
|
||||
|
||||
"""The firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the TP-Link router is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
"""Ensure the information from the TP-Link router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
with self.lock:
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
@@ -106,34 +98,24 @@ class TplinkDeviceScanner(object):
|
||||
|
||||
|
||||
class Tplink2DeviceScanner(TplinkDeviceScanner):
|
||||
"""
|
||||
This class queries a wireless router running newer version of TP-Link
|
||||
firmware for connected devices.
|
||||
"""
|
||||
"""This class queries a router with newer version of TP-Link firmware."""
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device ids.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return self.last_results.keys()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""
|
||||
The TP-Link firmware doesn't save the name of the wireless device.
|
||||
"""
|
||||
|
||||
"""The firmware doesn't save the name of the wireless device."""
|
||||
return self.last_results.get(device)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the TP-Link router is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
"""Ensure the information from the TP-Link router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
with self.lock:
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
@@ -171,46 +153,36 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
|
||||
|
||||
|
||||
class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
"""
|
||||
This class queries the Archer C9 router running version 150811 or higher
|
||||
of TP-Link firmware for connected devices.
|
||||
"""
|
||||
"""This class queries the Archer C9 router with version 150811 or high."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.stok = ''
|
||||
self.sysauth = ''
|
||||
super(Tplink3DeviceScanner, self).__init__(config)
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device ids.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return self.last_results.keys()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""
|
||||
The TP-Link firmware doesn't save the name of the wireless device.
|
||||
"""The firmware doesn't save the name of the wireless device.
|
||||
|
||||
We are forced to use the MAC address as name here.
|
||||
"""
|
||||
|
||||
return self.last_results.get(device)
|
||||
|
||||
def _get_auth_tokens(self):
|
||||
"""
|
||||
Retrieves auth tokens from the router.
|
||||
"""
|
||||
|
||||
"""Retrieve auth tokens from the router."""
|
||||
_LOGGER.info("Retrieving auth tokens...")
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/;stok=/login?form=login' \
|
||||
.format(self.host)
|
||||
referer = 'http://{}/webpages/login.html'.format(self.host)
|
||||
|
||||
# if possible implement rsa encryption of password here
|
||||
|
||||
# If possible implement rsa encryption of password here.
|
||||
response = requests.post(url,
|
||||
params={'operation': 'login',
|
||||
'username': self.username,
|
||||
@@ -231,19 +203,18 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the TP-Link router is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
"""Ensure the information from the TP-Link router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
with self.lock:
|
||||
if (self.stok == '') or (self.sysauth == ''):
|
||||
self._get_auth_tokens()
|
||||
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/;stok={}/admin/wireless?form=statistics' \
|
||||
.format(self.host, self.stok)
|
||||
url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?'
|
||||
'form=statistics').format(self.host, self.stok)
|
||||
referer = 'http://{}/webpages/index.html'.format(self.host)
|
||||
|
||||
response = requests.post(url,
|
||||
@@ -280,3 +251,81 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class Tplink4DeviceScanner(TplinkDeviceScanner):
|
||||
"""This class queries an Archer C7 router with TP-Link firmware 150427."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.credentials = ''
|
||||
self.token = ''
|
||||
super(Tplink4DeviceScanner, self).__init__(config)
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return self.last_results
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""The firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
def _get_auth_tokens(self):
|
||||
"""Retrieve auth tokens from the router."""
|
||||
_LOGGER.info("Retrieving auth tokens...")
|
||||
url = 'http://{}/userRpm/LoginRpm.htm?Save=Save'.format(self.host)
|
||||
|
||||
# Generate md5 hash of password
|
||||
password = hashlib.md5(self.password.encode('utf')).hexdigest()
|
||||
credentials = '{}:{}'.format(self.username, password).encode('utf')
|
||||
|
||||
# Encode the credentials to be sent as a cookie.
|
||||
self.credentials = base64.b64encode(credentials).decode('utf')
|
||||
|
||||
# Create the authorization cookie.
|
||||
cookie = 'Authorization=Basic {}'.format(self.credentials)
|
||||
|
||||
response = requests.get(url, headers={'cookie': cookie})
|
||||
|
||||
try:
|
||||
result = re.search(r'window.parent.location.href = '
|
||||
r'"https?:\/\/.*\/(.*)\/userRpm\/Index.htm";',
|
||||
response.text)
|
||||
if not result:
|
||||
return False
|
||||
self.token = result.group(1)
|
||||
return True
|
||||
except ValueError:
|
||||
_LOGGER.error("Couldn't fetch auth tokens!")
|
||||
return False
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the TP-Link router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
with self.lock:
|
||||
if (self.credentials == '') or (self.token == ''):
|
||||
self._get_auth_tokens()
|
||||
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
url = 'http://{}/{}/userRpm/WlanStationRpm.htm' \
|
||||
.format(self.host, self.token)
|
||||
referer = 'http://{}'.format(self.host)
|
||||
cookie = 'Authorization=Basic {}'.format(self.credentials)
|
||||
|
||||
page = requests.get(url, headers={
|
||||
'cookie': cookie,
|
||||
'referer': referer
|
||||
})
|
||||
result = self.parse_macs.findall(page.text)
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
self.last_results = [mac.replace("-", ":") for mac in result]
|
||||
return True
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.ubus
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a OpenWRT router for device
|
||||
presence.
|
||||
Support for OpenWRT (ubus) routers.
|
||||
|
||||
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
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Luci scanner. """
|
||||
"""Validate the configuration and return an ubus scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
@@ -40,23 +38,13 @@ def get_scanner(hass, config):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class UbusDeviceScanner(object):
|
||||
"""
|
||||
This class queries a wireless router running OpenWrt firmware
|
||||
for connected devices. Adapted from Tomato scanner.
|
||||
|
||||
Configure your routers' ubus ACL based on following instructions:
|
||||
|
||||
http://wiki.openwrt.org/doc/techref/ubus
|
||||
|
||||
Read only access will be fine.
|
||||
|
||||
To use this class you have to install rpcd-mod-file package
|
||||
in your OpenWrt router:
|
||||
|
||||
opkg install rpcd-mod-file
|
||||
This class queries a wireless router running OpenWrt firmware.
|
||||
|
||||
Adapted from Tomato scanner.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
|
||||
@@ -72,17 +60,12 @@ class UbusDeviceScanner(object):
|
||||
self.success_init = self.session_id is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device ids.
|
||||
"""
|
||||
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return self.last_results
|
||||
|
||||
def get_device_name(self, device):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
if self.leasefile is None:
|
||||
result = _req_json_rpc(self.url, self.session_id,
|
||||
@@ -111,8 +94,8 @@ class UbusDeviceScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the Luci router is up to date.
|
||||
"""Ensure the information from the Luci router is up to date.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
@@ -140,8 +123,7 @@ class UbusDeviceScanner(object):
|
||||
|
||||
|
||||
def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
|
||||
""" Perform one JSON RPC operation. """
|
||||
|
||||
"""Perform one JSON RPC operation."""
|
||||
data = json.dumps({"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": rpcmethod,
|
||||
@@ -166,7 +148,7 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
|
||||
|
||||
|
||||
def _get_session_id(url, username, password):
|
||||
""" Get authentication token for the given host+username+password. """
|
||||
"""Get the authentication token for the given host+username+password."""
|
||||
res = _req_json_rpc(url, "00000000000000000000000000000000", 'call',
|
||||
'session', 'login', username=username,
|
||||
password=password)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Support for Unifi WAP controllers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.unifi/
|
||||
"""
|
||||
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):
|
||||
"""Setup 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):
|
||||
"""Initialize the scanner."""
|
||||
self._controller = controller
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
"""Get the clients from the device."""
|
||||
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):
|
||||
"""Scan for devices."""
|
||||
self._update()
|
||||
return self._clients.keys()
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""Return 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,12 +11,11 @@ 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"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['netdisco==0.5.1']
|
||||
REQUIREMENTS = ['netdisco==0.5.4']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
@@ -30,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',
|
||||
@@ -40,26 +37,40 @@ 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. """
|
||||
"""Start a discovery service."""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from netdisco.service import DiscoveryService
|
||||
@@ -70,13 +81,13 @@ def setup(hass, config):
|
||||
lock = threading.Lock()
|
||||
|
||||
def new_service_listener(service, info):
|
||||
""" Called when a new service is found. """
|
||||
"""Called when a new service is found."""
|
||||
with lock:
|
||||
logger.info("Found new service: %s %s", service, info)
|
||||
|
||||
component = SERVICE_HANDLERS.get(service)
|
||||
|
||||
# We do not know how to handle this service
|
||||
# We do not know how to handle this service.
|
||||
if not component:
|
||||
return
|
||||
|
||||
@@ -91,7 +102,7 @@ def setup(hass, config):
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def start_discovery(event):
|
||||
""" Start discovering. """
|
||||
"""Start discovering."""
|
||||
netdisco = DiscoveryService(SCAN_INTERVAL)
|
||||
netdisco.add_listener(new_service_listener)
|
||||
netdisco.start()
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
"""
|
||||
homeassistant.components.downloader
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides functionality to download files.
|
||||
Support for 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
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import sanitize_filename
|
||||
|
||||
DOMAIN = "downloader"
|
||||
DEPENDENCIES = []
|
||||
|
||||
SERVICE_DOWNLOAD_FILE = "download_file"
|
||||
|
||||
@@ -27,18 +26,9 @@ CONF_DOWNLOAD_DIR = 'download_dir'
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
""" Listens for download events to download files. """
|
||||
|
||||
"""Listen for download events to download files."""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
logger.exception(("Failed to import requests. "
|
||||
"Did you maybe not execute 'pip install requests'?"))
|
||||
|
||||
return False
|
||||
|
||||
if not validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger):
|
||||
return False
|
||||
|
||||
@@ -57,14 +47,13 @@ def setup(hass, config):
|
||||
return False
|
||||
|
||||
def download_file(service):
|
||||
""" Starts thread to download file specified in the url. """
|
||||
|
||||
"""Start thread to download file specified in the URL."""
|
||||
if ATTR_URL not in service.data:
|
||||
logger.error("Service called but 'url' parameter not specified.")
|
||||
return
|
||||
|
||||
def do_download():
|
||||
""" Downloads the file. """
|
||||
"""Download the file."""
|
||||
try:
|
||||
url = service.data[ATTR_URL]
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Support for Ecobee.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ecobee/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
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"
|
||||
DISCOVER_SENSORS = "ecobee.sensor"
|
||||
NETWORK = None
|
||||
HOLD_TEMP = 'hold_temp'
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/nkgilley/python-ecobee-api/archive/'
|
||||
'92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
||||
_CONFIGURING = {}
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
|
||||
|
||||
|
||||
def request_configuration(network, hass, config):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = get_component('configurator')
|
||||
if 'ecobee' in _CONFIGURING:
|
||||
configurator.notify_errors(
|
||||
_CONFIGURING['ecobee'], "Failed to register, please try again.")
|
||||
|
||||
return
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def ecobee_configuration_callback(callback_data):
|
||||
"""The actions to do when our configuration callback is called."""
|
||||
network.request_tokens()
|
||||
network.update()
|
||||
setup_ecobee(hass, network, config)
|
||||
|
||||
_CONFIGURING['ecobee'] = configurator.request_config(
|
||||
hass, "Ecobee", ecobee_configuration_callback,
|
||||
description=(
|
||||
'Please authorize this app at https://www.ecobee.com/consumer'
|
||||
'portal/index.html with pin code: ' + network.pin),
|
||||
description_image="/static/images/config_ecobee_thermostat.png",
|
||||
submit_caption="I have authorized the app."
|
||||
)
|
||||
|
||||
|
||||
def setup_ecobee(hass, network, config):
|
||||
"""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)
|
||||
return
|
||||
|
||||
if 'ecobee' in _CONFIGURING:
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(_CONFIGURING.pop('ecobee'))
|
||||
|
||||
# Ensure component is loaded
|
||||
bootstrap.setup_component(hass, 'thermostat', config)
|
||||
bootstrap.setup_component(hass, 'sensor', config)
|
||||
|
||||
hold_temp = config[DOMAIN].get(HOLD_TEMP, False)
|
||||
|
||||
# Fire thermostat discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: DISCOVER_THERMOSTAT,
|
||||
ATTR_DISCOVERED: {'hold_temp': hold_temp}
|
||||
})
|
||||
|
||||
# Fire sensor discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: DISCOVER_SENSORS,
|
||||
ATTR_DISCOVERED: {}
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class EcobeeData(object):
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, config_file):
|
||||
"""Initialize the Ecobee data object."""
|
||||
from pyecobee import Ecobee
|
||||
self.ecobee = Ecobee(config_file)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from pyecobee."""
|
||||
self.ecobee.update()
|
||||
_LOGGER.info("ecobee data updated successfully.")
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup Ecobee.
|
||||
|
||||
Will automatically load thermostat and sensor components to support
|
||||
devices discovered on the network.
|
||||
"""
|
||||
# pylint: disable=global-statement, import-error
|
||||
global NETWORK
|
||||
|
||||
if 'ecobee' in _CONFIGURING:
|
||||
return
|
||||
|
||||
from pyecobee import config_from_file
|
||||
|
||||
# Create ecobee.conf if it doesn't exist
|
||||
if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)):
|
||||
if config[DOMAIN].get(CONF_API_KEY) is None:
|
||||
_LOGGER.error("No ecobee api_key found in config.")
|
||||
return
|
||||
jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)}
|
||||
config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
|
||||
|
||||
NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE))
|
||||
|
||||
setup_ecobee(hass, NETWORK.ecobee, config)
|
||||
|
||||
return True
|
||||
@@ -1,9 +1,4 @@
|
||||
"""
|
||||
homeassistant.components.frontend
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides a frontend for Home Assistant.
|
||||
"""
|
||||
"""Handle the frontend for Home Assistant."""
|
||||
import re
|
||||
import os
|
||||
import logging
|
||||
@@ -11,7 +6,7 @@ import logging
|
||||
from . import version, mdi_version
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import URL_ROOT, HTTP_OK
|
||||
from homeassistant.config import get_default_config_dir
|
||||
from homeassistant.components import api
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
@@ -22,22 +17,26 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FRONTEND_URLS = [
|
||||
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent', '/devInfo']
|
||||
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
|
||||
'/devEvent', '/devInfo', '/devTemplate',
|
||||
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
|
||||
|
||||
"""Setup serving the frontend."""
|
||||
for url in FRONTEND_URLS:
|
||||
hass.http.register_path('GET', url, _handle_get_root, False)
|
||||
|
||||
hass.http.register_path('GET', STATES_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(
|
||||
@@ -53,9 +52,20 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_root(handler, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
def _handle_get_api_bootstrap(handler, path_match, data):
|
||||
"""Return 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):
|
||||
"""Render the frontend."""
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header('Content-type', 'text/html; charset=utf-8')
|
||||
handler.end_headers()
|
||||
@@ -66,7 +76,7 @@ def _handle_get_root(handler, path_match, data):
|
||||
app_url = "frontend-{}.html".format(version.VERSION)
|
||||
|
||||
# auto login if no password was set, else check api_password param
|
||||
auth = ('no_password_set' if handler.server.no_password_set
|
||||
auth = ('no_password_set' if handler.server.api_password is None
|
||||
else data.get('api_password', ''))
|
||||
|
||||
with open(INDEX_PATH) as template_file:
|
||||
@@ -79,8 +89,19 @@ def _handle_get_root(handler, path_match, data):
|
||||
handler.wfile.write(template_html.encode("UTF-8"))
|
||||
|
||||
|
||||
def _handle_get_service_worker(handler, path_match, data):
|
||||
"""Return service worker for the frontend."""
|
||||
if handler.server.development:
|
||||
sw_path = "home-assistant-polymer/build/service_worker.js"
|
||||
else:
|
||||
sw_path = "service_worker.js"
|
||||
|
||||
handler.write_file(os.path.join(os.path.dirname(__file__), 'www_static',
|
||||
sw_path))
|
||||
|
||||
|
||||
def _handle_get_static(handler, path_match, data):
|
||||
""" Returns a static file for the frontend. """
|
||||
"""Return a static file for the frontend."""
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
|
||||
# Strip md5 hash out
|
||||
@@ -94,13 +115,9 @@ def _handle_get_static(handler, path_match, data):
|
||||
|
||||
|
||||
def _handle_get_local(handler, path_match, data):
|
||||
"""
|
||||
Returns a static file from the hass.config.path/www for the frontend.
|
||||
"""
|
||||
"""Return a static file from the hass.config.path/www for the frontend."""
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
|
||||
path = os.path.join(get_default_config_dir(), 'www', req_file)
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
path = handler.server.hass.config.path('www', req_file)
|
||||
|
||||
handler.write_file(path)
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home Assistant</title>
|
||||
|
||||
<link rel='manifest' href='/static/manifest.json' />
|
||||
<link rel='shortcut icon' href='/static/favicon.ico' />
|
||||
<link rel='icon' type='image/png'
|
||||
href='/static/favicon-192x192.png' sizes='192x192'>
|
||||
<link rel='manifest' href='/static/manifest.json'>
|
||||
<link rel='icon' href='/static/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/favicon-apple-180x180.png'>
|
||||
href='/static/favicon-apple-180x180.png'>
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='viewport' content='width=device-width,
|
||||
user-scalable=no' />
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='#03a9f4'>
|
||||
<style>
|
||||
#init {
|
||||
#ha-init-skeleton {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
@@ -26,26 +23,29 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
#init div {
|
||||
line-height: 34px;
|
||||
margin-bottom: 89px;
|
||||
margin-bottom: 123px;
|
||||
}
|
||||
</style>
|
||||
<link rel='import' href='/static/{{ app_url }}' async>
|
||||
</head>
|
||||
<body fullbleed>
|
||||
<div id='init'>
|
||||
<img src='/static/splash.png' height='230' />
|
||||
<div>Initializing</div>
|
||||
</div>
|
||||
<script src='/static/webcomponents-lite.min.js'></script>
|
||||
<link rel='import' href='/static/{{ app_url }}' />
|
||||
<div id='ha-init-skeleton'><img src='/static/favicon-192x192.png' height='192'></div>
|
||||
<script>
|
||||
var webComponentsSupported = ('registerElement' in document &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'))
|
||||
if (!webComponentsSupported) {
|
||||
var script = document.createElement('script')
|
||||
script.async = true
|
||||
script.src = '/static/webcomponents-lite.min.js'
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
</script>
|
||||
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user