Compare commits
991 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e44ed0090 | |||
| 5fb6076f6e | |||
| 4ea6def1bd | |||
| 314185ffb8 | |||
| 8aafb89a64 | |||
| d01ed9788f | |||
| b4e2f4f0be | |||
| f4d8325084 | |||
| cc5dec3c59 | |||
| ec5a93a0fd | |||
| aabda1b7b0 | |||
| aee4411cfb | |||
| 135eb0a0ac | |||
| 01daac066a | |||
| 5d96ca133d | |||
| 3cda1aacff | |||
| 243130c133 | |||
| 0d74b628b0 | |||
| c314101dde | |||
| 0f68dc6b7b | |||
| 919c20a263 | |||
| 9b5385c565 | |||
| 7dacf01baa | |||
| 9b4650afd4 | |||
| 0b0fd2490d | |||
| 3a85bebbf6 | |||
| 18f1de10a5 | |||
| 869d6df65e | |||
| 0a586bd919 | |||
| a98b1b0ebc | |||
| 511028612c | |||
| e3efce5ded | |||
| 340ee171b5 | |||
| 773da3f755 | |||
| 12bdc39274 | |||
| 700b7ba591 | |||
| 295f27d259 | |||
| 4463b69245 | |||
| 71e4283a2e | |||
| 6135b87b1d | |||
| 04bb7ed58f | |||
| dfa9880176 | |||
| 88f3a5a50a | |||
| e2c530b85d | |||
| 56c5d345a4 | |||
| 86b9ae9566 | |||
| df264f2ec0 | |||
| aabcad59d8 | |||
| 70fef3c5b5 | |||
| 57ec58e255 | |||
| cf8e23adbc | |||
| eabf9087f3 | |||
| 9acb341b96 | |||
| 5275ca9ce7 | |||
| 646618a25e | |||
| bc48e4f98e | |||
| 776324807e | |||
| d68a4b52f1 | |||
| 85e0db6ade | |||
| d993f4014e | |||
| 7ebda9c3c6 | |||
| 16e948d032 | |||
| 41d0f95d9a | |||
| bfaaf39e9e | |||
| 16904452b8 | |||
| 158d9e27ff | |||
| b652dd47cd | |||
| 2812fae721 | |||
| 6da88108fe | |||
| 5503c12cfd | |||
| 329d63ac11 | |||
| 698e30bd2b | |||
| 90063ea7f8 | |||
| 0c52b143ae | |||
| 8f12b997f8 | |||
| 877926cfee | |||
| 19649390d3 | |||
| 50d19bb1b4 | |||
| 665436cd91 | |||
| 1a3410119e | |||
| bf2bcb6dcf | |||
| 8371b08676 | |||
| e8a0d54fdd | |||
| f4a82c6f6b | |||
| 963c4bb70e | |||
| ec2e0cc77d | |||
| 0c0ccb361d | |||
| 994fc32f25 | |||
| d68263d5c4 | |||
| 27b001df2b | |||
| dafc0ced6b | |||
| 3ec2555c66 | |||
| bfa8e58879 | |||
| 2d9a785c18 | |||
| 97f9f8aa49 | |||
| 64d5ca4da0 | |||
| 4fb301b7a9 | |||
| fda65a4934 | |||
| 98b4c27211 | |||
| 3b3f5fe6fe | |||
| 1c9c5ce1bd | |||
| a36b315927 | |||
| f3352546c6 | |||
| 3a6aa8f3d1 | |||
| dfa81b0117 | |||
| 3947691347 | |||
| 8541fdb112 | |||
| e078ab53ca | |||
| 0665af7f0f | |||
| 6dfb8f5737 | |||
| 1be2be0886 | |||
| 3a095f53a8 | |||
| ffce252a12 | |||
| 7f4c13c382 | |||
| 0f292e8fa6 | |||
| e63d0c51e0 | |||
| ebaecdb9d6 | |||
| fd50693ca7 | |||
| ebc95aca51 | |||
| 137cadb59c | |||
| 84f81480bb | |||
| 5565e418f8 | |||
| 5b4fc4f346 | |||
| e4c3d47dbf | |||
| 95320f39b3 | |||
| 0c97280479 | |||
| 3e339c7304 | |||
| 6f06f48ac6 | |||
| aeacbad4a0 | |||
| f60f3fa4a2 | |||
| e49dc94d4b | |||
| 26a6438e93 | |||
| cae71a73a1 | |||
| df7f6e1235 | |||
| a31f7d2816 | |||
| c52c982510 | |||
| ea06d946e6 | |||
| bd798b8c55 | |||
| ac7456b73b | |||
| d69b08ecf5 | |||
| be6dd20236 | |||
| 4d069323f4 | |||
| 77f4fc8c22 | |||
| 72b4212b19 | |||
| 5fce381b89 | |||
| 7b968f6a6b | |||
| 4d958c6d18 | |||
| 566712023d | |||
| 3cd89f8474 | |||
| 218a05356a | |||
| 186f68cce3 | |||
| 168eb8e5a2 | |||
| c6b5a04312 | |||
| 728cd8bb5e | |||
| d873ab0262 | |||
| 0ff6a460c2 | |||
| 77539a5b89 | |||
| 92b05389f2 | |||
| 82aec895a0 | |||
| ec732becfc | |||
| c4261ae2e0 | |||
| cae8932b18 | |||
| efacd66bec | |||
| 31826ab263 | |||
| 6bb95f5c9b | |||
| 46761d827f | |||
| 288db9a57f | |||
| b76471c4b3 | |||
| 194c6343ac | |||
| 2ad647bb09 | |||
| a56173676e | |||
| e961dd5f95 | |||
| 0269be5813 | |||
| 031d5ce255 | |||
| 85bb828149 | |||
| c7a0b5800c | |||
| f456d2ff23 | |||
| 6bad702db4 | |||
| bcb2451752 | |||
| 8eeca94517 | |||
| 3b37a7b737 | |||
| d578bf3494 | |||
| 8cb046a4a9 | |||
| 30de5af445 | |||
| f74a6ed4c9 | |||
| f3500542dd | |||
| 2eb65c8eb8 | |||
| f8bb807707 | |||
| 5f40115605 | |||
| 776616bcac | |||
| a98cb798f7 | |||
| 12495c717e | |||
| 10c95b4352 | |||
| 48bfc98acb | |||
| de027609d8 | |||
| 16a3511c0a | |||
| 0a36c96a55 | |||
| 6ef0d089ea | |||
| b3b2f2e326 | |||
| bef0b2b01e | |||
| 805aecd6f9 | |||
| e4d33bc6d4 | |||
| e25503bc4a | |||
| 4fcd27e905 | |||
| 52b1080ccd | |||
| 0128357024 | |||
| c5f8095f53 | |||
| ab48a94d2a | |||
| 27c6c27db6 | |||
| 55c0ee6b32 | |||
| 6b881ce1cd | |||
| 9ab9d0e383 | |||
| dbc05450a0 | |||
| b66e4f1e15 | |||
| 649124d162 | |||
| 49f4d92c62 | |||
| fbb73dd5da | |||
| c9f1dce6a2 | |||
| ef6c209c6f | |||
| 0826ae2742 | |||
| 18747f8ae1 | |||
| 06c8c1b168 | |||
| 160fb6fcc8 | |||
| f1aa685cf2 | |||
| da259d75a2 | |||
| 9104ca815d | |||
| 52193611cd | |||
| a2256f6c97 | |||
| 004bad7f00 | |||
| a8c2cc4c33 | |||
| 538f8545f7 | |||
| 7cfce94dfb | |||
| e490388843 | |||
| fb8edca942 | |||
| 469d0619ba | |||
| bc8c5766d4 | |||
| 5b25d9ccd6 | |||
| 0d05930765 | |||
| f93282d636 | |||
| 78ad2686d4 | |||
| 3c36d13e8d | |||
| 77ba0c0393 | |||
| 6a3316ed12 | |||
| 415d650860 | |||
| c3c248bc0a | |||
| bf027fcd48 | |||
| 5c79fc0ae3 | |||
| 0aaf280bf5 | |||
| 55de563511 | |||
| 1a018e3ee7 | |||
| b023348795 | |||
| 847d9736aa | |||
| 884525df33 | |||
| 6a82504e5e | |||
| db7e46abd1 | |||
| 8e9cafd29d | |||
| a236b87ccf | |||
| e379e3d902 | |||
| 1be48f54c0 | |||
| 96181a555a | |||
| e461ceae36 | |||
| 83e6c24f18 | |||
| 50fbd83b3d | |||
| 649275044a | |||
| 0f81fc60ad | |||
| ade8681511 | |||
| 7a699fd819 | |||
| b0c0659acc | |||
| 80a9422a9a | |||
| 060cbaf66b | |||
| c19120e012 | |||
| 1e0e48fcd7 | |||
| f2fda2914a | |||
| 7e3483ab03 | |||
| bffce11a9a | |||
| 8a895390ef | |||
| 97f81ad7a6 | |||
| 67d5581a1b | |||
| 3406b41b0c | |||
| 2e7912157b | |||
| 5dbdf82ec7 | |||
| 9f4a3f4aea | |||
| 4f3b3a9e34 | |||
| 3ea167203f | |||
| 756cbe1b08 | |||
| 87e55820e7 | |||
| f828ee044d | |||
| a8e2f9cbb7 | |||
| 2e3f462474 | |||
| 3f6780d9be | |||
| e21921823e | |||
| f9b2e0058e | |||
| a155587693 | |||
| 0e145ec130 | |||
| 44b08a06e7 | |||
| 75f737144a | |||
| 170742b0a7 | |||
| 6115be7c42 | |||
| 84a9a300d6 | |||
| 55718aac66 | |||
| b6e6512367 | |||
| 29c9c5a7ec | |||
| dd787ea5ce | |||
| c2d75efb4d | |||
| 3d972abdab | |||
| 7801489149 | |||
| 0fda89e983 | |||
| 07a75c5eeb | |||
| 0b7c407519 | |||
| 3c34f3dac2 | |||
| d45074f9dc | |||
| 863955e1bd | |||
| 4ff1b0fdb2 | |||
| ba13f13442 | |||
| e615755eb9 | |||
| 352d3532e7 | |||
| 7b60f6ca77 | |||
| a2e8fcbc77 | |||
| e10fd0d28b | |||
| f376061e23 | |||
| 076b3db5e8 | |||
| 89964ad793 | |||
| bddd02bd58 | |||
| cfb3384ee3 | |||
| 490e9ee95d | |||
| 0d0eb7e7c0 | |||
| 3d4af8c229 | |||
| 7e23c241da | |||
| 7ec1424825 | |||
| 9162149598 | |||
| da31b54d06 | |||
| b20a757454 | |||
| 761f225c86 | |||
| 5580309d98 | |||
| 52b4c3b5a2 | |||
| aed61cecff | |||
| ef129639bd | |||
| f945a3a692 | |||
| 5e56eae28f | |||
| 72ad1387f0 | |||
| 02cfc70ad5 | |||
| 5a21b677a1 | |||
| c1a73d250a | |||
| 8c544a89c9 | |||
| c473d426e3 | |||
| f5a62f8381 | |||
| 0e8e4a73fe | |||
| 74700e4b10 | |||
| 4d5c9581bf | |||
| f45e0eabe3 | |||
| bbed4a262c | |||
| f8590f7d1d | |||
| 293ed275ab | |||
| 2e9ee28637 | |||
| 73cb23f599 | |||
| 661f4c594e | |||
| 9464e2a13b | |||
| 27d5248937 | |||
| c5a7e3abd1 | |||
| 0a6424a81d | |||
| 5b7389de55 | |||
| 0fe4e0330a | |||
| 40b095b866 | |||
| 7141a99927 | |||
| f01d2b1263 | |||
| 3c6420c538 | |||
| e3304caf06 | |||
| 91a1fb0240 | |||
| 6fd32e83c8 | |||
| b41caa093c | |||
| 806c71c803 | |||
| b0bafa32b7 | |||
| 332ac794a4 | |||
| 5dfd0d2502 | |||
| 3d838c307f | |||
| 2dd77f9477 | |||
| 6c106a87f1 | |||
| 1279bf7c68 | |||
| 323d301072 | |||
| b1815075ac | |||
| 7ba4263284 | |||
| 347597ebdc | |||
| ddeb84cb9c | |||
| b2e39884f9 | |||
| 8d99c4a0cc | |||
| 80e4f2f51f | |||
| 6064fffc8e | |||
| 7da354c4c5 | |||
| 49de153ecf | |||
| 716376081d | |||
| d37b70556d | |||
| 32bb950b5f | |||
| 0e89418cbe | |||
| fe032be352 | |||
| 64a78d7b4f | |||
| 24e4b9e012 | |||
| a44a39003d | |||
| f019b4f697 | |||
| e353dae3a6 | |||
| fb84c0ce6b | |||
| fd382871a1 | |||
| 405025a00b | |||
| 912ddbb4fc | |||
| 91138b8679 | |||
| 893b9fc8ac | |||
| 185ba000dd | |||
| 771118caaf | |||
| 17d9df0da5 | |||
| e067398134 | |||
| cb69ac30ec | |||
| 241ff45c5e | |||
| c5c2f0c5f3 | |||
| 0874cb364f | |||
| 44418b509c | |||
| 403889bbeb | |||
| 6ca50d8b5c | |||
| 0369a9ee0d | |||
| 5a6ff9a69a | |||
| 796cce78bc | |||
| 77430c0687 | |||
| c33942d6e2 | |||
| 3b91f89173 | |||
| c74f46794e | |||
| 966fd8f24d | |||
| 8253fdfc13 | |||
| cb7b5f8d15 | |||
| b0d8eaeda9 | |||
| 62cfb8aeb2 | |||
| 47448d1dc0 | |||
| a583525110 | |||
| 38e1cef30e | |||
| e2f187879c | |||
| 925cde200f | |||
| 383efee470 | |||
| d5eb90160f | |||
| 2f946cc6de | |||
| 1b7ce2146c | |||
| 021a374a6a | |||
| bbec34d0e6 | |||
| a6cb19b27d | |||
| b74e70d4e0 | |||
| 5cd283e999 | |||
| 2f2bd7a616 | |||
| cddc87b0ab | |||
| d6bbc67112 | |||
| 6d77b15e44 | |||
| 916c453d2b | |||
| 7786b52d93 | |||
| 90d4a2c0b8 | |||
| b6d26597c0 | |||
| bf1970b78c | |||
| 1eb3610a11 | |||
| f081f7c4ff | |||
| 6a969208e9 | |||
| 384b3d0d17 | |||
| ad5b650661 | |||
| b05f2e3221 | |||
| dcfc91e71c | |||
| 6c1c243000 | |||
| c2117b3eaf | |||
| 84c72ebf63 | |||
| a1e5bea3ab | |||
| bee5c0adfb | |||
| 7ca21f577d | |||
| 168516f5da | |||
| 94df5acbf3 | |||
| 853a9fd4cd | |||
| 6a18205d2e | |||
| 3a3b8bbb45 | |||
| 37278aab20 | |||
| d3c4722529 | |||
| c3de67041a | |||
| f07d07432d | |||
| 47f994b867 | |||
| a8a172c8b7 | |||
| d4d91bfdbb | |||
| 7432bbd70c | |||
| a3d295d885 | |||
| 0cf909cce9 | |||
| fc1cf49fd3 | |||
| e5d68d8a1e | |||
| 8fc2f5fe36 | |||
| c2c18bdbd5 | |||
| f8efe3f00f | |||
| db53e46705 | |||
| 526a163563 | |||
| e29f857f43 | |||
| 9f6ce868e2 | |||
| 3ef5e7c161 | |||
| fe5bb89a68 | |||
| be8089bcde | |||
| 47fc1deecb | |||
| 8a04e1f5f4 | |||
| 9f33b8f541 | |||
| 0624725e21 | |||
| dc5f0ef314 | |||
| cb2943c247 | |||
| 45f0911640 | |||
| 4ac9e9fc4c | |||
| 28b107ffa9 | |||
| e0149c4ee4 | |||
| 455a5916fd | |||
| cbf94aae55 | |||
| 39ced09727 | |||
| ad417bfdfb | |||
| f682fd7c1f | |||
| ee23c0fe14 | |||
| 5322789c14 | |||
| 05cec772d0 | |||
| 75c3e42064 | |||
| 61c955779b | |||
| a015df7b01 | |||
| 721c1d0f54 | |||
| fe37a6aecc | |||
| 85bf6cb568 | |||
| 9f10ab5e7a | |||
| 3b7f6d3b67 | |||
| d8aefb5d55 | |||
| 06cac7f9ef | |||
| 6d3f18d094 | |||
| 050f90d07a | |||
| bf9b179441 | |||
| 4f0f7eff5e | |||
| 4edbdab4c0 | |||
| 729f59625e | |||
| 213a1fe4ba | |||
| c1899609a4 | |||
| 1b4ef3856a | |||
| 4673a82c90 | |||
| ae6f651c7d | |||
| 7cb0f805ee | |||
| d556e5979a | |||
| d149f9d64c | |||
| 9377b647f5 | |||
| 469f35d25f | |||
| 17865c78c4 | |||
| a5dae78155 | |||
| 46f5ef54a1 | |||
| 496e4cf784 | |||
| 11fc521e60 | |||
| a58382e763 | |||
| 9d4aa7e519 | |||
| ffbaf0cd5a | |||
| 3b58e8628d | |||
| c2fe977778 | |||
| 85338887b4 | |||
| 9a3c76c263 | |||
| 6ab4b80486 | |||
| 5e0a4c316f | |||
| 26939ce554 | |||
| c83324d4cf | |||
| dbcc3a76ea | |||
| faa3e98921 | |||
| 1c4ac6017d | |||
| 25a690691b | |||
| bb997deb85 | |||
| 6c4b2fd638 | |||
| c4f8017a3f | |||
| 6afb846d04 | |||
| ad549be353 | |||
| 9012ba53fd | |||
| bdb42bf4a2 | |||
| 1b22f71a19 | |||
| e84ddb036f | |||
| 4be33bb15b | |||
| d17174d43d | |||
| e64846e2fd | |||
| d454cad5a6 | |||
| dcf52332ca | |||
| 87599df41b | |||
| e535f50e03 | |||
| df7fbf664e | |||
| 0fb9e1b16c | |||
| 047cff6596 | |||
| 32f1791c5a | |||
| 3b49d1e876 | |||
| 4d1dce2519 | |||
| 01d097b9b0 | |||
| 6d53944fa1 | |||
| 7f60f1e662 | |||
| bc6c285945 | |||
| d4d8c9ae65 | |||
| 9292891836 | |||
| 035df68d6c | |||
| c611be96ad | |||
| d46720ee2c | |||
| c1f464f478 | |||
| 8c5759e460 | |||
| 8490d6126a | |||
| 7f71706f08 | |||
| 8b5b580287 | |||
| d35f5b9f97 | |||
| 6de64d7695 | |||
| 7f1da8b7bc | |||
| 58ac4be24c | |||
| b2919c6504 | |||
| db509ccf18 | |||
| 3863d2985a | |||
| 0180c056e1 | |||
| e6cd9a6dc7 | |||
| 33028dd143 | |||
| 9bdfa89b7c | |||
| 42b80868d4 | |||
| 707ca4b752 | |||
| c7d2a09097 | |||
| fb9f83f8ad | |||
| 52ebb2fb3b | |||
| ea7ca48ba2 | |||
| ab80af099c | |||
| 34531895a0 | |||
| 645cd89406 | |||
| cc5217d818 | |||
| e454806669 | |||
| 726557b2f6 | |||
| c7e22e6910 | |||
| f66a020bfc | |||
| 64a73f6b67 | |||
| ad7f034805 | |||
| 76674d4de9 | |||
| 0dc9f2a9f8 | |||
| ce47b58a8b | |||
| 5d71d5560e | |||
| 1dc9bfdf73 | |||
| cc47e39006 | |||
| 2eb36c18bd | |||
| d64f0ddd41 | |||
| 5ad27d8cdb | |||
| 68c2b539ee | |||
| e57b3ae847 | |||
| 755234369d | |||
| 0a34e8de02 | |||
| 52ed25fc21 | |||
| 9e866680d4 | |||
| 80c89d218b | |||
| 4f1bf7b2bf | |||
| e557e355db | |||
| 174aeacd76 | |||
| e7320fe969 | |||
| 321a603bfe | |||
| 4e3bd5f2a9 | |||
| 9a6b2c1831 | |||
| ca0b6ebd99 | |||
| 47cd0b20a0 | |||
| 98d051f870 | |||
| 5f98705100 | |||
| 63bf4db969 | |||
| 3ec00ce4fe | |||
| 74a0e47ba6 | |||
| 476e4f0517 | |||
| 61fb8271e5 | |||
| 5cf9bd7223 | |||
| 9f986c55e6 | |||
| 94eb54ff00 | |||
| f28b392f1a | |||
| fa71d5fac9 | |||
| 21fd53b05d | |||
| b0b3c2f73f | |||
| d660d2b3dc | |||
| a89bfcf342 | |||
| a42347e6e7 | |||
| faee3e8447 | |||
| 3625646c34 | |||
| 5a562f3db8 | |||
| 19705ab40a | |||
| 20bf9f7ea1 | |||
| 6399c873f9 | |||
| 4be1053f1c | |||
| efdd0c9e8a | |||
| 8d42e42230 | |||
| 4b6878f91c | |||
| 90f35b35cd | |||
| 082920abe0 | |||
| 4a8bbc52e0 | |||
| 1674c8309a | |||
| 62f016e7d2 | |||
| 34e5ecb8ab | |||
| 8f95885e3a | |||
| 94db1ac142 | |||
| f48e65096a | |||
| 3244975489 | |||
| 10327795e9 | |||
| bcbb8edd59 | |||
| 86270e1a37 | |||
| de7a34b648 | |||
| 82a06279de | |||
| 62af1fcc57 | |||
| 6afe99dcc7 | |||
| b6bf398859 | |||
| 48df06d1c0 | |||
| b4ca691822 | |||
| 16c2827465 | |||
| e90fd3d654 | |||
| 7d0ff6884c | |||
| a9ea8972dd | |||
| a0c1202ad6 | |||
| 1bf45c8f33 | |||
| c5094438de | |||
| 60d45ebf79 | |||
| 5df2a1cf76 | |||
| f5b2fa6fbe | |||
| 03b2ced24e | |||
| 6c18f264f3 | |||
| cdc371c3ee | |||
| 1553844279 | |||
| 826b3be087 | |||
| f5000d401b | |||
| 7443f4faf8 | |||
| 582ed1fc8d | |||
| 3158db9553 | |||
| 46a0173e31 | |||
| 7e511bcacf | |||
| d7fd2ccdaf | |||
| b2999ae325 | |||
| 5033c1fcb7 | |||
| e492be299b | |||
| 03e7281406 | |||
| 16d75b2981 | |||
| cc7784889a | |||
| d267f0a04c | |||
| a8e0ca6d3f | |||
| f8175adbdc | |||
| 6437f6f6b4 | |||
| 27bbfbae62 | |||
| 2785c373fb | |||
| acddae3747 | |||
| d3e9a22759 | |||
| ca698ff063 | |||
| a866d515f7 | |||
| 3af4f267b3 | |||
| bd61555698 | |||
| 5027acfda1 | |||
| f0991d63d1 | |||
| 34f36479c6 | |||
| 506c88dbaf | |||
| 98a1addc18 | |||
| 30492cc685 | |||
| 0d09e2e1df | |||
| 81085c7467 | |||
| 19d40612e6 | |||
| 48306ddbf6 | |||
| a60a9202a5 | |||
| ab81231e6d | |||
| 68286dcef8 | |||
| 6e96f915f6 | |||
| 620a7eadf4 | |||
| 2332548cf4 | |||
| dc55525206 | |||
| 9318b36ac2 | |||
| 46f6653183 | |||
| 9736761968 | |||
| 6352f10d9e | |||
| 2a3b911d7b | |||
| 720e5876a7 | |||
| 85489010bc | |||
| 60d8266ce0 | |||
| e29deb0202 | |||
| 268b0f17d0 | |||
| 49ce85f2e4 | |||
| 1771f8b1b3 | |||
| 8cd1c42e80 | |||
| ec1d5e617e | |||
| 40651ef2bc | |||
| 64741a95b8 | |||
| a24b38aacc | |||
| 55f6ff86e4 | |||
| 79cdda2bd9 | |||
| 35eed93443 | |||
| dd4e1cbd1d | |||
| 2084976bc2 | |||
| 9019d654d7 | |||
| e0f6239ef3 | |||
| b9e1b3eb99 | |||
| e1a7b8f988 | |||
| be9cfbdeb0 | |||
| a32229b4ce | |||
| 7da104af4e | |||
| 9d7aef94e0 | |||
| e4c5108c9d | |||
| fc946da5db | |||
| b33714bca3 | |||
| 722af9014d | |||
| 6abaebb248 | |||
| c01e9bea2b | |||
| 5ce4ade737 | |||
| 9ce8f385d2 | |||
| d7464aea86 | |||
| 071952462c | |||
| ab79b8a541 | |||
| 3a3374ed4b | |||
| 4d53fa0173 | |||
| 5369d8c61c | |||
| 6c1f44242c | |||
| 4371355be1 | |||
| 5bb88909a0 | |||
| 4b0c416844 | |||
| 737d7c9d22 | |||
| 15be5ced9a | |||
| 7c549db2d6 | |||
| 6e6aa15f7c | |||
| e0c1885a71 | |||
| 049cd159ce | |||
| 95e05d4fc9 | |||
| bf14067eb0 | |||
| 8c77418b6a | |||
| d25a42426a | |||
| 3ed102cd88 | |||
| 90e2aefd23 | |||
| 47af247d6a | |||
| 3947ed3c2b | |||
| 1a00d4a095 | |||
| ccecc0181d | |||
| e90dbad37e | |||
| 8ec0c36457 | |||
| e68cc83e64 | |||
| 4ad4d74ed4 | |||
| 550f31d4c3 | |||
| 7e42b35b62 | |||
| 9b96471182 | |||
| 3c3eadbef5 | |||
| f375bc527a | |||
| 6de04d78ed | |||
| 86aea83f64 | |||
| 98feb3cd93 | |||
| 5af1643297 | |||
| 3dcd18af9e | |||
| 2fd7b98cab | |||
| 90e21791f6 | |||
| 9678613a13 | |||
| 5de89316b2 | |||
| 95eabe7c0e | |||
| 9bec0316ea | |||
| 61685ea13d | |||
| 77b9a12687 | |||
| 08f2a67de4 | |||
| 58c3b03b79 | |||
| c18294ee76 | |||
| 408f0cff78 | |||
| 0584c10ef9 | |||
| ae527e9c6f | |||
| 1ec5178f66 | |||
| 2978e0dabe | |||
| e26f0f7b7d | |||
| 2ff2a78e97 | |||
| bb172d8c98 | |||
| acb288f9e7 | |||
| c7565baa6d | |||
| fb29611c15 | |||
| 37cd62447e | |||
| 5cbcd72912 | |||
| 8bba0b88fd | |||
| b1f17c2cd4 | |||
| 8017f7f241 | |||
| 1a73c1b991 | |||
| 039c5cd847 | |||
| 3b27bef1ac | |||
| 1fc2204ca9 | |||
| 834ce5269d | |||
| 9ada5e6b2b | |||
| f17ef0327c | |||
| 56a151b196 | |||
| 6e927d68e5 | |||
| fcad068016 | |||
| e12cc2fbbf | |||
| 9588fcc5cc | |||
| a4aa2e4383 | |||
| fe074835f0 | |||
| b2ad8db86b | |||
| 20f021d05f | |||
| fc43135ddd | |||
| e86ee9eae7 | |||
| 332f7621ce | |||
| 68c1dd7cd4 | |||
| fe2a9bb83e | |||
| 2f8591205f | |||
| 65c3184856 | |||
| 0afb6114c5 | |||
| 7c7b6ca05c | |||
| 2fe8b154f1 | |||
| bf64956265 | |||
| eb11486e76 | |||
| e8c3eaab33 | |||
| fcbeddeb57 | |||
| 50b23e1969 | |||
| 984f01359c | |||
| d5198d4242 | |||
| f5d1da1d53 | |||
| 13ca42e187 | |||
| 74eb577c58 | |||
| fe7134b897 | |||
| 27845d3fc5 | |||
| 6606d2a73c | |||
| 6dc877d8de | |||
| b0441aadc4 | |||
| dd71e4fdd1 | |||
| 7e066e11ad | |||
| 13d40fe6ec | |||
| 7e75add144 | |||
| 2df26a0d1a | |||
| 965730eb60 | |||
| 4c0ac6051f | |||
| 2a11d02fe4 | |||
| 046c5653cb | |||
| f86fcdcaf5 | |||
| 835bc1c492 | |||
| de5a2fee83 | |||
| 209499e82b | |||
| 9b47241a46 | |||
| 513f6e9c3c | |||
| 9582eae48e | |||
| d4834ff408 | |||
| ce22f3c82d | |||
| 246184507c | |||
| 3f3b475d76 | |||
| 40aa661340 | |||
| 6c3a78df30 | |||
| 964a1f9aef | |||
| 8360ab265c | |||
| e3dcb45879 | |||
| 683a80f5f4 | |||
| 9904727cde | |||
| e9da02d70c | |||
| b0b88e606c | |||
| 57a833f1a7 | |||
| e5e577108c | |||
| 51dd718282 | |||
| 40340ea832 | |||
| a2ca60159d | |||
| 50f5f1860c | |||
| 8e89308a15 | |||
| 96cfff192a | |||
| 067993c8ab | |||
| eef1e65244 | |||
| 134c870d2b | |||
| 5edc4f148f | |||
| 880b5f0ad1 | |||
| 804b7669b7 | |||
| 81288cc988 | |||
| d4174f5e42 | |||
| cfc23b0091 | |||
| bb42e264cb | |||
| c9bccadc40 | |||
| ab6cb43d5b | |||
| 4fa379419d | |||
| b01ff81b47 | |||
| 6dcb87c54d | |||
| 6cfca09daf | |||
| 4eba1250e9 | |||
| d4d798d71f | |||
| 3dc1dc6c6a | |||
| 473047f3dd | |||
| f5b5d3f65a | |||
| 776c7dae07 | |||
| 4ccedca3e5 | |||
| d9b97ad5b4 | |||
| de89de890f | |||
| 5338b29edf | |||
| 395dbe8804 | |||
| f41786d893 | |||
| 34dee0c134 | |||
| 705238eb78 | |||
| 2b6e0da405 | |||
| 9d750368ff | |||
| 7252861b83 | |||
| db2140782f | |||
| b9f5ec9e2c | |||
| 6d9b618f1c | |||
| a459368998 | |||
| cb3f14a862 | |||
| e9367d5369 | |||
| c3dd94ba04 | |||
| 6624cfefd6 | |||
| 350ed9f764 | |||
| 3679a8078a | |||
| a25f7eed2b | |||
| ae058b7847 | |||
| aa74c4e57a | |||
| 1b874c603b | |||
| 050fe809e1 | |||
| e2b02f2fd2 | |||
| 775d3198ae | |||
| 21812ba717 | |||
| 2d54fdd979 | |||
| e093abc366 | |||
| 5d3e929599 | |||
| 1ec392a494 | |||
| d719dd72fe | |||
| 53b43dc4db | |||
| f21d97d5a2 | |||
| f9b17ab026 | |||
| e88fabbe6d | |||
| 0509b478e9 |
+26
-1
@@ -26,6 +26,9 @@ omit =
|
||||
homeassistant/components/zwave.py
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/rfxtrx.py
|
||||
homeassistant/components/*/rfxtrx.py
|
||||
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/*
|
||||
@@ -33,39 +36,56 @@ omit =
|
||||
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/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/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.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/hyperion.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
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/sonos.py
|
||||
homeassistant/components/notify/file.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/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/xmpp.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/dht.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/mysensors.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/rfxtrx.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/swiss_public_transport.py
|
||||
@@ -73,13 +93,18 @@ omit =
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/command_switch.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.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/radiotherm.py
|
||||
|
||||
|
||||
[report]
|
||||
|
||||
+3
-4
@@ -15,10 +15,6 @@ tests/config/home-assistant.log
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Hide code validator output
|
||||
pep8.txt
|
||||
pylint.txt
|
||||
|
||||
# Hide some OS X stuff
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
@@ -30,6 +26,9 @@ Icon
|
||||
|
||||
.idea
|
||||
|
||||
# pytest
|
||||
.cache
|
||||
|
||||
# GITHUB Proposed Python stuff:
|
||||
*.py[cod]
|
||||
|
||||
|
||||
+5
-7
@@ -1,13 +1,11 @@
|
||||
sudo: false
|
||||
language: python
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/virtualenv/python3.4.2/
|
||||
python:
|
||||
- "3.4"
|
||||
install:
|
||||
- pip install -r requirements_all.txt
|
||||
- pip install flake8 pylint coveralls
|
||||
- script/bootstrap_server
|
||||
script:
|
||||
- flake8 homeassistant
|
||||
- pylint homeassistant
|
||||
- coverage run -m unittest discover tests
|
||||
after_success:
|
||||
- coveralls
|
||||
- script/cibuild
|
||||
|
||||
+8
-8
@@ -17,19 +17,19 @@ For help on building your component, please see the [developer documentation](ht
|
||||
|
||||
After you finish adding support for your device:
|
||||
|
||||
- Update the supported devices in the `README.md` file.
|
||||
- Add any new dependencies to `requirements.txt`.
|
||||
- Update the `.coveragerc` file.
|
||||
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io).
|
||||
- Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`.
|
||||
- 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.
|
||||
- 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`.
|
||||
- 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/).
|
||||
|
||||
If you've added a component:
|
||||
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:
|
||||
|
||||
- Update the file [`home-assistant-icons.html`](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html) with an icon for your domain ([pick one from this list](https://www.polymer-project.org/1.0/components/core-elements/demo.html#core-icon)).
|
||||
- Update the demo component with two states that it provides
|
||||
- Add your component to home-assistant.conf.example
|
||||
- Update the demo component with two states that it provides.
|
||||
- Add your component to `home-assistant.conf.example`.
|
||||
|
||||
Since you've updated `home-assistant-icons.html`, you've made changes to the frontend:
|
||||
|
||||
|
||||
+5
-6
@@ -10,11 +10,10 @@ RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nmap net-tools && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Open Z-Wave disabled because broken
|
||||
#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 && \
|
||||
# scripts/build_python_openzwave
|
||||
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
|
||||
|
||||
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]
|
||||
|
||||
+5
-1
@@ -1 +1,5 @@
|
||||
recursive-exclude tests *
|
||||
include README.md
|
||||
include LICENSE
|
||||
graft homeassistant
|
||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
@@ -16,10 +16,11 @@ Check out [the website](https://home-assistant.io) for [a demo][demo], installat
|
||||
|
||||
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/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/)
|
||||
* [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, RFXtrx sensors, 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), and [Kodi (XBMC)](http://kodi.tv/)
|
||||
* 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/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
|
||||
* 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/)
|
||||
|
||||
@@ -29,8 +30,8 @@ Built home automation on top of your devices:
|
||||
* 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
|
||||
* 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/), and [Jabber (XMPP)](http://xmpp.org)
|
||||
* 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).
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
homeassistant:
|
||||
# Omitted values in this section will be auto detected using freegeoip.net
|
||||
|
||||
# Location required to calculate the time the sun rises and sets
|
||||
# Location required to calculate the time the sun rises and sets.
|
||||
# Cooridinates are also used for location for weather related components.
|
||||
# Google Maps can be used to determine more precise GPS cooridinates.
|
||||
latitude: 32.87336
|
||||
longitude: 117.22743
|
||||
|
||||
@@ -68,11 +70,18 @@ device_sun_light_trigger:
|
||||
|
||||
# A comma separated list of states that have to be tracked as a single group
|
||||
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
|
||||
# You can also have groups within groups.
|
||||
group:
|
||||
Home:
|
||||
- group.living_room
|
||||
- group.kitchen
|
||||
living_room:
|
||||
- light.Bowl
|
||||
- light.Ceiling
|
||||
- light.TV_back_light
|
||||
kitchen:
|
||||
- light.fan_bulb_1
|
||||
- light.fan_bulb_2
|
||||
children:
|
||||
- device_tracker.child_1
|
||||
- device_tracker.child_2
|
||||
@@ -94,28 +103,39 @@ browser:
|
||||
keyboard:
|
||||
|
||||
automation:
|
||||
platform: state
|
||||
alias: Sun starts shining
|
||||
- alias: 'Rule 1 Light on in the evening'
|
||||
trigger:
|
||||
- platform: sun
|
||||
event: sunset
|
||||
offset: "-01:00:00"
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
condition:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
- platform: time
|
||||
after: "16:00:00"
|
||||
before: "23:00:00"
|
||||
action:
|
||||
service: homeassistant.turn_on
|
||||
entity_id: group.living_room
|
||||
|
||||
state_entity_id: sun.sun
|
||||
# Next two are optional, omit to match all
|
||||
state_from: below_horizon
|
||||
state_to: above_horizon
|
||||
- alias: 'Rule 2 - Away Mode'
|
||||
|
||||
execute_service: light.turn_off
|
||||
service_entity_id: group.living_room
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: 'not_home'
|
||||
|
||||
automation 2:
|
||||
platform: time
|
||||
alias: Beer o Clock
|
||||
condition: use_trigger_values
|
||||
action:
|
||||
service: light.turn_off
|
||||
entity_id: group.all_lights
|
||||
|
||||
time_hours: 16
|
||||
time_minutes: 0
|
||||
time_seconds: 0
|
||||
|
||||
execute_service: notify.notify
|
||||
service_data:
|
||||
message: It's 4, time for beer!
|
||||
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
|
||||
# Each sensor label should be unique or your sensors might not load correctly.
|
||||
|
||||
sensor:
|
||||
platform: systemmonitor
|
||||
@@ -135,6 +155,23 @@ sensor:
|
||||
- type: 'process'
|
||||
arg: 'octave-cli'
|
||||
|
||||
sensor 2:
|
||||
platform: forecast
|
||||
api_key: <register on Forecast.io for your PRIVATE API>
|
||||
monitored_conditions:
|
||||
- summary
|
||||
- precip_type
|
||||
- precip_intensity
|
||||
- temperature
|
||||
- dew_point
|
||||
- wind_speed
|
||||
- wind_bearing
|
||||
- cloud_cover
|
||||
- humidity
|
||||
- pressure
|
||||
- visibility
|
||||
- ozone
|
||||
|
||||
script:
|
||||
# Turns on the bedroom lights and then the living room lights 1 minute later
|
||||
wakeup:
|
||||
|
||||
@@ -95,6 +95,18 @@ def get_arguments():
|
||||
type=int,
|
||||
default=None,
|
||||
help='Enables daily log rotation and keeps up to the specified days')
|
||||
parser.add_argument(
|
||||
'--install-osx',
|
||||
action='store_true',
|
||||
help='Installs as a service on OS X and loads on boot.')
|
||||
parser.add_argument(
|
||||
'--uninstall-osx',
|
||||
action='store_true',
|
||||
help='Uninstalls from OS X.')
|
||||
parser.add_argument(
|
||||
'--restart-osx',
|
||||
action='store_true',
|
||||
help='Restarts on OS X.')
|
||||
if os.name != "nt":
|
||||
parser.add_argument(
|
||||
'--daemon',
|
||||
@@ -152,6 +164,46 @@ def write_pid(pid_file):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def install_osx():
|
||||
""" Setup to run via launchd on OS X """
|
||||
with os.popen('which hass') as inp:
|
||||
hass_path = inp.read().strip()
|
||||
|
||||
with os.popen('whoami') as inp:
|
||||
user = inp.read().strip()
|
||||
|
||||
cwd = os.path.dirname(__file__)
|
||||
template_path = os.path.join(cwd, 'startup', 'launchd.plist')
|
||||
|
||||
with open(template_path, 'r', encoding='utf-8') as inp:
|
||||
plist = inp.read()
|
||||
|
||||
plist = plist.replace("$HASS_PATH$", hass_path)
|
||||
plist = plist.replace("$USER$", user)
|
||||
|
||||
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
|
||||
|
||||
try:
|
||||
with open(path, 'w', encoding='utf-8') as outp:
|
||||
outp.write(plist)
|
||||
except IOError as err:
|
||||
print('Unable to write to ' + path, err)
|
||||
return
|
||||
|
||||
os.popen('launchctl load -w -F ' + path)
|
||||
|
||||
print("Home Assistant has been installed. \
|
||||
Open it here: http://localhost:8123")
|
||||
|
||||
|
||||
def uninstall_osx():
|
||||
""" 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()
|
||||
@@ -161,6 +213,18 @@ def main():
|
||||
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)
|
||||
|
||||
+44
-11
@@ -9,11 +9,12 @@ After bootstrapping you can add your own components or
|
||||
start by calling homeassistant.start_home_assistant(bus)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
import logging.handlers
|
||||
from collections import defaultdict
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
import homeassistant.core as core
|
||||
import homeassistant.util.dt as date_util
|
||||
@@ -25,7 +26,7 @@ import homeassistant.components as core_components
|
||||
import homeassistant.components.group as group
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
|
||||
__version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
|
||||
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_CUSTOMIZE,
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
||||
|
||||
@@ -34,6 +35,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
PLATFORM_FORMAT = '{}.{}'
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
|
||||
|
||||
def setup_component(hass, domain, config=None):
|
||||
@@ -123,6 +125,7 @@ def prepare_setup_platform(hass, config, domain, platform_name):
|
||||
|
||||
# Not found
|
||||
if platform is None:
|
||||
_LOGGER.error('Unable to find platform %s', platform_path)
|
||||
return None
|
||||
|
||||
# Already loaded
|
||||
@@ -166,6 +169,7 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
process_ha_config_upgrade(hass)
|
||||
process_ha_core_config(hass, config.get(core.DOMAIN, {}))
|
||||
|
||||
if enable_log:
|
||||
@@ -185,8 +189,8 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
dict, {key: value or {} for key, value in config.items()})
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
components = (key for key in config.keys()
|
||||
if ' ' not in key and key != core.DOMAIN)
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
@@ -251,7 +255,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
|
||||
"Colorlog package not found, console coloring disabled")
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
err_log_path = hass.config.path('home-assistant.log')
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
err_path_exists = os.path.isfile(err_log_path)
|
||||
|
||||
# Check if we can write to the error log if it exists or that
|
||||
@@ -279,6 +283,31 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
|
||||
'Unable to setup error log %s (access denied)', err_log_path)
|
||||
|
||||
|
||||
def process_ha_config_upgrade(hass):
|
||||
""" Upgrade config if necessary. """
|
||||
version_path = hass.config.path('.HA_VERSION')
|
||||
|
||||
try:
|
||||
with open(version_path, 'rt') as inp:
|
||||
conf_version = inp.readline().strip()
|
||||
except FileNotFoundError:
|
||||
# Last version to not have this file
|
||||
conf_version = '0.7.7'
|
||||
|
||||
if conf_version == __version__:
|
||||
return
|
||||
|
||||
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
|
||||
__version__)
|
||||
|
||||
lib_path = hass.config.path('lib')
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
with open(version_path, 'wt') as outp:
|
||||
outp.write(__version__)
|
||||
|
||||
|
||||
def process_ha_core_config(hass, config):
|
||||
""" Processes the [homeassistant] section from the config. """
|
||||
hac = hass.config
|
||||
@@ -296,11 +325,15 @@ def process_ha_core_config(hass, config):
|
||||
else:
|
||||
_LOGGER.error('Received invalid time zone %s', time_zone_str)
|
||||
|
||||
for key, attr in ((CONF_LATITUDE, 'latitude'),
|
||||
(CONF_LONGITUDE, 'longitude'),
|
||||
(CONF_NAME, 'location_name')):
|
||||
for key, attr, typ in ((CONF_LATITUDE, 'latitude', float),
|
||||
(CONF_LONGITUDE, 'longitude', float),
|
||||
(CONF_NAME, 'location_name', str)):
|
||||
if key in config:
|
||||
setattr(hac, attr, config[key])
|
||||
try:
|
||||
setattr(hac, attr, typ(config[key]))
|
||||
except ValueError:
|
||||
_LOGGER.error('Received invalid %s value for %s: %s',
|
||||
typ.__name__, key, attr)
|
||||
|
||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
homeassistant.components
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This package contains components that can be plugged into Home Assistant.
|
||||
|
||||
Component design guidelines:
|
||||
@@ -12,7 +11,6 @@ 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
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
homeassistant.components.alarm_control_panel
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with a alarm control panel.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components import verisure
|
||||
from homeassistant.const import (
|
||||
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'
|
||||
}
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_ALARM_DISARM: 'alarm_disarm',
|
||||
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
|
||||
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
|
||||
SERVICE_ALARM_TRIGGER: 'alarm_trigger'
|
||||
}
|
||||
|
||||
ATTR_CODE = 'code'
|
||||
ATTR_CODE_FORMAT = 'code_format'
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
ATTR_CODE,
|
||||
ATTR_CODE_FORMAT
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for sensors. """
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
def alarm_service_handler(service):
|
||||
""" Maps services to methods on Alarm. """
|
||||
target_alarms = component.extract_from_service(service)
|
||||
|
||||
if ATTR_CODE not in service.data:
|
||||
code = None
|
||||
else:
|
||||
code = service.data[ATTR_CODE]
|
||||
|
||||
method = SERVICE_TO_METHOD[service.service]
|
||||
|
||||
for alarm in target_alarms:
|
||||
getattr(alarm, method)(code)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.register(DOMAIN, service, alarm_service_handler,
|
||||
descriptions.get(service))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def alarm_disarm(hass, code=None, entity_id=None):
|
||||
""" Send the alarm the command for disarm. """
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data)
|
||||
|
||||
|
||||
def alarm_arm_home(hass, code=None, entity_id=None):
|
||||
""" Send the alarm the command for arm home. """
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data)
|
||||
|
||||
|
||||
def alarm_arm_away(hass, code=None, entity_id=None):
|
||||
""" Send the alarm the command for arm away. """
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
|
||||
|
||||
|
||||
def alarm_trigger(hass, code=None, entity_id=None):
|
||||
""" Send the alarm the command for trigger. """
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
""" ABC for alarm control devices. """
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" regex for code format or None if no code is required. """
|
||||
return None
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
""" Send alarm trigger command. """
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
}
|
||||
return state_attr
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
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
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
|
||||
|
||||
_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. """
|
||||
|
||||
add_devices([ManualAlarm(
|
||||
hass,
|
||||
config.get('name', DEFAULT_ALARM_NAME),
|
||||
config.get('code'),
|
||||
config.get('pending_time', DEFAULT_PENDING_TIME),
|
||||
config.get('trigger_time', DEFAULT_TRIGGER_TIME),
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class ManualAlarm(alarm.AlarmControlPanel):
|
||||
"""
|
||||
Represents an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for 'trigger_time'. After that will be
|
||||
triggered for 'trigger_time', after that we return to disarmed.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time, trigger_time):
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._pending_time = datetime.timedelta(seconds=pending_time)
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
self._state_ts = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns 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 \
|
||||
self._pending_time and self._state_ts + self._pending_time > \
|
||||
dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state_ts + self._pending_time > dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" One or more characters. """
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
if not self._validate_code(code, STATE_ALARM_DISARMED):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
""" Send alarm trigger command. No code needed. """
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
|
||||
if self._trigger_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._state_ts + self._pending_time + self._trigger_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
""" Validate given code. """
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Invalid code given for %s', state)
|
||||
return check
|
||||
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
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
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "MQTT Alarm"
|
||||
DEFAULT_QOS = 0
|
||||
DEFAULT_PAYLOAD_DISARM = "DISARM"
|
||||
DEFAULT_PAYLOAD_ARM_HOME = "ARM_HOME"
|
||||
DEFAULT_PAYLOAD_ARM_AWAY = "ARM_AWAY"
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the MQTT platform. """
|
||||
|
||||
if config.get('state_topic') is None:
|
||||
_LOGGER.error("Missing required variable: state_topic")
|
||||
return False
|
||||
|
||||
if config.get('command_topic') is None:
|
||||
_LOGGER.error("Missing required variable: command_topic")
|
||||
return False
|
||||
|
||||
add_devices([MqttAlarm(
|
||||
hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('state_topic'),
|
||||
config.get('command_topic'),
|
||||
config.get('qos', DEFAULT_QOS),
|
||||
config.get('payload_disarm', DEFAULT_PAYLOAD_DISARM),
|
||||
config.get('payload_arm_home', DEFAULT_PAYLOAD_ARM_HOME),
|
||||
config.get('payload_arm_away', DEFAULT_PAYLOAD_ARM_AWAY),
|
||||
config.get('code'))])
|
||||
|
||||
|
||||
# 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. """
|
||||
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code):
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._code = str(code) if code else None
|
||||
|
||||
def message_received(topic, payload, qos):
|
||||
""" 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):
|
||||
_LOGGER.warning('Received unexpected payload: %s', payload)
|
||||
return
|
||||
self._state = payload
|
||||
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):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" 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. """
|
||||
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. """
|
||||
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. """
|
||||
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. """
|
||||
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,97 @@
|
||||
"""
|
||||
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/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.verisure as verisure
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Verisure platform. """
|
||||
|
||||
if not verisure.MY_PAGES:
|
||||
_LOGGER.error('A connection has not been made to Verisure mypages.')
|
||||
return False
|
||||
|
||||
alarms = []
|
||||
|
||||
alarms.extend([
|
||||
VerisureAlarm(value)
|
||||
for value in verisure.get_alarm_status().values()
|
||||
if verisure.SHOW_ALARM
|
||||
])
|
||||
|
||||
add_devices(alarms)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
""" Represents a Verisure alarm status. """
|
||||
|
||||
def __init__(self, alarm_status):
|
||||
self._id = alarm_status.id
|
||||
self._device = verisure.MY_PAGES.DEVICE_ALARM
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return 'Alarm {}'.format(self._id)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" Four digit code required. """
|
||||
return '^\\d{4}$'
|
||||
|
||||
def update(self):
|
||||
""" Update alarm status """
|
||||
verisure.update()
|
||||
|
||||
if verisure.STATUS[self._device][self._id].status == 'unarmed':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedhome':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedaway':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif verisure.STATUS[self._device][self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
'Unknown alarm state %s',
|
||||
verisure.STATUS[self._device][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')
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.api
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides a 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
|
||||
@@ -12,13 +14,14 @@ import json
|
||||
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_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)
|
||||
HTTP_UNPROCESSABLE_ENTITY, CONTENT_TYPE_TEXT_PLAIN)
|
||||
|
||||
|
||||
DOMAIN = 'api'
|
||||
@@ -87,6 +90,9 @@ def setup(hass, config):
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_COMPONENTS, _handle_get_api_components)
|
||||
|
||||
hass.http.register_path('GET', URL_API_ERROR_LOG,
|
||||
_handle_get_api_error_log)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -103,6 +109,10 @@ def _handle_get_api_stream(handler, path_match, data):
|
||||
write_lock = threading.Lock()
|
||||
block = threading.Event()
|
||||
|
||||
restrict = data.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',')
|
||||
|
||||
def write_message(payload):
|
||||
""" Writes a message to the output. """
|
||||
with write_lock:
|
||||
@@ -118,7 +128,8 @@ def _handle_get_api_stream(handler, path_match, data):
|
||||
""" Forwards events to the open request. """
|
||||
nonlocal gracefully_closed
|
||||
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED or \
|
||||
restrict and event.event_type not in restrict:
|
||||
return
|
||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
gracefully_closed = True
|
||||
@@ -334,6 +345,13 @@ def _handle_get_api_components(handler, path_match, data):
|
||||
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)
|
||||
|
||||
|
||||
def _services_json(hass):
|
||||
""" Generate services data to JSONify. """
|
||||
return [{"domain": key, "services": value}
|
||||
|
||||
@@ -4,26 +4,8 @@ components.arduino
|
||||
Arduino component that connects to a directly attached Arduino board which
|
||||
runs with the Firmata firmware.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Arduino board you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
arduino:
|
||||
port: /dev/ttyACM0
|
||||
|
||||
Variables:
|
||||
|
||||
port
|
||||
*Required
|
||||
The port where is your board connected to your Home Assistant system.
|
||||
If you are using an original Arduino the port will be named ttyACM*. The exact
|
||||
number can be determined with 'ls /dev/ttyACM*' or check your 'dmesg'/
|
||||
'journalctl -f' output. Keep in mind that Arduino clones are often using a
|
||||
different name for the port (e.g. '/dev/ttyUSB*').
|
||||
|
||||
A word of caution: The Arduino is not storing states. This means that with
|
||||
every initialization the pins are set to off/low.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/arduino/
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
@@ -1,74 +1,225 @@
|
||||
"""
|
||||
homeassistant.components.automation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Allows 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/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.util import split_entity_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
from homeassistant.components import logbook
|
||||
|
||||
DOMAIN = "automation"
|
||||
DOMAIN = 'automation'
|
||||
|
||||
DEPENDENCIES = ["group"]
|
||||
DEPENDENCIES = ['group']
|
||||
|
||||
CONF_ALIAS = "alias"
|
||||
CONF_SERVICE = "execute_service"
|
||||
CONF_SERVICE_ENTITY_ID = "service_entity_id"
|
||||
CONF_SERVICE_DATA = "service_data"
|
||||
CONF_ALIAS = 'alias'
|
||||
CONF_SERVICE = 'service'
|
||||
CONF_SERVICE_ENTITY_ID = 'entity_id'
|
||||
CONF_SERVICE_DATA = 'data'
|
||||
|
||||
CONF_CONDITION = 'condition'
|
||||
CONF_ACTION = 'action'
|
||||
CONF_TRIGGER = 'trigger'
|
||||
CONF_CONDITION_TYPE = 'condition_type'
|
||||
|
||||
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
|
||||
CONDITION_TYPE_AND = 'and'
|
||||
CONDITION_TYPE_OR = 'or'
|
||||
|
||||
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up automation. """
|
||||
success = False
|
||||
config_key = DOMAIN
|
||||
found = 1
|
||||
|
||||
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
|
||||
while config_key in config:
|
||||
# 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)
|
||||
|
||||
if platform is None:
|
||||
_LOGGER.error("Unknown automation platform specified: %s", p_type)
|
||||
continue
|
||||
# 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)
|
||||
|
||||
if platform.register(hass, p_config, _get_action(hass, p_config)):
|
||||
_LOGGER.info(
|
||||
"Initialized %s rule %s", p_type, p_config.get(CONF_ALIAS, ""))
|
||||
success = True
|
||||
# any scalar value is incorrect
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Error setting up rule %s", p_config.get(CONF_ALIAS, ""))
|
||||
_LOGGER.error('Error in config in section %s.', config_key)
|
||||
|
||||
return success
|
||||
found += 1
|
||||
config_key = "{} {}".format(DOMAIN, found)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _get_action(hass, config):
|
||||
def _setup_automation(hass, config_block, name, config):
|
||||
""" Setup one instance of automation """
|
||||
|
||||
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)
|
||||
|
||||
if action is None:
|
||||
return False
|
||||
|
||||
if CONF_CONDITION in config_block or CONF_CONDITION_TYPE in config_block:
|
||||
action = _process_if(hass, config, config_block, action)
|
||||
|
||||
if action is None:
|
||||
return False
|
||||
|
||||
_process_trigger(hass, config, config_block.get(CONF_TRIGGER, []), name,
|
||||
action)
|
||||
return True
|
||||
|
||||
|
||||
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 None
|
||||
|
||||
def action():
|
||||
""" Action to be executed. """
|
||||
_LOGGER.info("Executing rule %s", config.get(CONF_ALIAS, ""))
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
|
||||
if CONF_SERVICE in config:
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
service_data = config.get(CONF_SERVICE_DATA, {})
|
||||
|
||||
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 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]
|
||||
|
||||
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)
|
||||
hass.services.call(domain, service, service_data)
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _migrate_old_config(config):
|
||||
""" Migrate old config 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')
|
||||
|
||||
new_conf = {
|
||||
CONF_TRIGGER: dict(config),
|
||||
CONF_CONDITION: config.get('if', []),
|
||||
CONF_ACTION: dict(config),
|
||||
}
|
||||
|
||||
for cat, key, new_key in (('trigger', 'mqtt_topic', 'topic'),
|
||||
('trigger', 'mqtt_payload', 'payload'),
|
||||
('trigger', 'state_entity_id', 'entity_id'),
|
||||
('trigger', 'state_before', 'before'),
|
||||
('trigger', 'state_after', 'after'),
|
||||
('trigger', 'state_to', 'to'),
|
||||
('trigger', 'state_from', 'from'),
|
||||
('trigger', 'state_hours', 'hours'),
|
||||
('trigger', 'state_minutes', 'minutes'),
|
||||
('trigger', 'state_seconds', 'seconds'),
|
||||
('action', 'execute_service', 'service'),
|
||||
('action', 'service_entity_id', 'entity_id'),
|
||||
('action', 'service_data', 'data')):
|
||||
if key in new_conf[cat]:
|
||||
new_conf[cat][new_key] = new_conf[cat].pop(key)
|
||||
|
||||
return new_conf
|
||||
|
||||
|
||||
def _process_if(hass, config, p_config, action):
|
||||
""" Processes if checks. """
|
||||
|
||||
cond_type = p_config.get(CONF_CONDITION_TYPE,
|
||||
DEFAULT_CONDITION_TYPE).lower()
|
||||
|
||||
if_configs = p_config.get(CONF_CONDITION)
|
||||
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
|
||||
|
||||
if use_trigger:
|
||||
if_configs = p_config[CONF_TRIGGER]
|
||||
|
||||
if isinstance(if_configs, dict):
|
||||
if_configs = [if_configs]
|
||||
|
||||
checks = []
|
||||
for if_config in if_configs:
|
||||
platform = _resolve_platform('if_action', hass, config,
|
||||
if_config.get(CONF_PLATFORM))
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
check = platform.if_action(hass, if_config)
|
||||
|
||||
# Invalid conditions are allowed if we base it on trigger
|
||||
if check is None and not use_trigger:
|
||||
return None
|
||||
|
||||
checks.append(check)
|
||||
|
||||
if cond_type == CONDITION_TYPE_AND:
|
||||
def if_action():
|
||||
""" AND all conditions. """
|
||||
if all(check() for check in checks):
|
||||
action()
|
||||
else:
|
||||
def if_action():
|
||||
""" OR all conditions. """
|
||||
if any(check() for check in checks):
|
||||
action()
|
||||
|
||||
return if_action
|
||||
|
||||
|
||||
def _process_trigger(hass, config, trigger_configs, name, action):
|
||||
""" Setup triggers. """
|
||||
if isinstance(trigger_configs, dict):
|
||||
trigger_configs = [trigger_configs]
|
||||
|
||||
for conf in trigger_configs:
|
||||
platform = _resolve_platform('trigger', hass, config,
|
||||
conf.get(CONF_PLATFORM))
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
if platform.trigger(hass, conf, action):
|
||||
_LOGGER.info("Initialized rule %s", name)
|
||||
else:
|
||||
_LOGGER.error("Error setting up rule %s", name)
|
||||
|
||||
|
||||
def _resolve_platform(method, hass, config, platform):
|
||||
""" Find automation platform. """
|
||||
if platform is None:
|
||||
return None
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
|
||||
|
||||
if platform is None or not hasattr(platform, method):
|
||||
_LOGGER.error("Unknown automation platform specified for %s: %s",
|
||||
method, platform)
|
||||
return None
|
||||
|
||||
return platform
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.automation.event
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers event listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#event-trigger
|
||||
"""
|
||||
import logging
|
||||
|
||||
@@ -12,7 +14,7 @@ CONF_EVENT_DATA = "event_data"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for events based on config. """
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
|
||||
@@ -20,11 +22,12 @@ def register(hass, config, action):
|
||||
_LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE)
|
||||
return False
|
||||
|
||||
event_data = config.get(CONF_EVENT_DATA, {})
|
||||
event_data = config.get(CONF_EVENT_DATA)
|
||||
|
||||
def handle_event(event):
|
||||
""" Listens for events and calls the action when data matches. """
|
||||
if event_data == event.data:
|
||||
if not event_data or all(val == event.data.get(key) for key, val
|
||||
in event_data.items()):
|
||||
action()
|
||||
|
||||
hass.bus.listen(event_type, handle_event)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.automation.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers MQTT listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||
"""
|
||||
import logging
|
||||
|
||||
@@ -10,11 +12,11 @@ import homeassistant.components.mqtt as mqtt
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_TOPIC = 'mqtt_topic'
|
||||
CONF_PAYLOAD = 'mqtt_payload'
|
||||
CONF_TOPIC = 'topic'
|
||||
CONF_PAYLOAD = 'payload'
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
topic = config.get(CONF_TOPIC)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
homeassistant.components.automation.numeric_state
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers numeric state listening automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_BELOW = "below"
|
||||
CONF_ABOVE = "above"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
|
||||
return False
|
||||
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
|
||||
if below is None and above is None:
|
||||
_LOGGER.error("Missing configuration key."
|
||||
" One of %s or %s is required",
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return False
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
""" Listens 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)):
|
||||
action()
|
||||
|
||||
track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with state based condition. """
|
||||
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
|
||||
return None
|
||||
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
|
||||
if below is None and above is None:
|
||||
_LOGGER.error("Missing configuration key."
|
||||
" One of %s or %s is required",
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return None
|
||||
|
||||
def if_numeric_state():
|
||||
""" Test numeric state condition. """
|
||||
state = hass.states.get(entity_id)
|
||||
return state is not None and _in_range(state.state, above, below)
|
||||
|
||||
return if_numeric_state
|
||||
|
||||
|
||||
def _in_range(value, range_start, range_end):
|
||||
""" Checks if value is inside the range """
|
||||
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
_LOGGER.warn("Missing value in numeric check")
|
||||
return False
|
||||
|
||||
if range_start is not None and range_end is not None:
|
||||
return float(range_start) <= value < float(range_end)
|
||||
elif range_end is not None:
|
||||
return value < float(range_end)
|
||||
else:
|
||||
return float(range_start) <= value
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.automation.state
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers 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
|
||||
|
||||
@@ -10,22 +12,28 @@ from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.const import MATCH_ALL
|
||||
|
||||
|
||||
CONF_ENTITY_ID = "state_entity_id"
|
||||
CONF_FROM = "state_from"
|
||||
CONF_TO = "state_to"
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_FROM = "from"
|
||||
CONF_TO = "to"
|
||||
CONF_STATE = "state"
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing configuration key %s", CONF_ENTITY_ID)
|
||||
"Missing trigger configuration key %s", CONF_ENTITY_ID)
|
||||
return False
|
||||
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
||||
|
||||
if isinstance(from_state, bool) or isinstance(to_state, bool):
|
||||
logging.getLogger(__name__).error(
|
||||
'Config error. Surround to/from values with quotes.')
|
||||
return False
|
||||
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
@@ -35,3 +43,23 @@ def register(hass, config, action):
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with state based condition. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
state = config.get(CONF_STATE)
|
||||
|
||||
if entity_id is None or state is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing if-condition configuration key %s or %s", CONF_ENTITY_ID,
|
||||
CONF_STATE)
|
||||
return None
|
||||
|
||||
state = str(state)
|
||||
|
||||
def if_state():
|
||||
""" Test if condition. """
|
||||
return hass.states.is_state(entity_id, state)
|
||||
|
||||
return if_state
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
homeassistant.components.automation.sun
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers sun based automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#sun-trigger
|
||||
"""
|
||||
import 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
|
||||
|
||||
DEPENDENCIES = ['sun']
|
||||
|
||||
CONF_OFFSET = 'offset'
|
||||
CONF_EVENT = 'event'
|
||||
|
||||
EVENT_SUNSET = 'sunset'
|
||||
EVENT_SUNRISE = 'sunrise'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for events based on config. """
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
if event is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_EVENT)
|
||||
return False
|
||||
|
||||
event = event.lower()
|
||||
if event not in (EVENT_SUNRISE, EVENT_SUNSET):
|
||||
_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)
|
||||
|
||||
# Do something to call action
|
||||
if event == EVENT_SUNRISE:
|
||||
trigger_sunrise(hass, action, offset)
|
||||
else:
|
||||
trigger_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
|
||||
|
||||
while next_time < dt_util.utcnow():
|
||||
next_time = next_time + timedelta(days=1)
|
||||
|
||||
return next_time
|
||||
|
||||
def sunrise_automation_listener(now):
|
||||
""" Called when it's time for action. """
|
||||
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
|
||||
action()
|
||||
|
||||
track_point_in_utc_time(hass, sunrise_automation_listener, next_rise())
|
||||
|
||||
|
||||
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
|
||||
|
||||
while next_time < dt_util.utcnow():
|
||||
next_time = next_time + timedelta(days=1)
|
||||
|
||||
return next_time
|
||||
|
||||
def sunset_automation_listener(now):
|
||||
""" Called when it's time for action. """
|
||||
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
|
||||
action()
|
||||
|
||||
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
|
||||
@@ -1,22 +1,46 @@
|
||||
"""
|
||||
homeassistant.components.automation.time
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers 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
|
||||
|
||||
CONF_HOURS = "time_hours"
|
||||
CONF_MINUTES = "time_minutes"
|
||||
CONF_SECONDS = "time_seconds"
|
||||
CONF_HOURS = "hours"
|
||||
CONF_MINUTES = "minutes"
|
||||
CONF_SECONDS = "seconds"
|
||||
CONF_BEFORE = "before"
|
||||
CONF_AFTER = "after"
|
||||
CONF_WEEKDAY = "weekday"
|
||||
|
||||
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
hours = convert(config.get(CONF_HOURS), int)
|
||||
minutes = convert(config.get(CONF_MINUTES), int)
|
||||
seconds = convert(config.get(CONF_SECONDS), int)
|
||||
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)
|
||||
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. """
|
||||
@@ -26,3 +50,58 @@ def register(hass, config, action):
|
||||
hour=hours, minute=minutes, second=seconds)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps 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(
|
||||
"Missing if-condition configuration key %s, %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY)
|
||||
return None
|
||||
|
||||
if before is not None:
|
||||
before = dt_util.parse_time_str(before)
|
||||
if before is None:
|
||||
_error_time(before, CONF_BEFORE)
|
||||
return None
|
||||
|
||||
if after is not None:
|
||||
after = dt_util.parse_time_str(after)
|
||||
if after is None:
|
||||
_error_time(after, CONF_AFTER)
|
||||
return None
|
||||
|
||||
def time_if():
|
||||
""" Validate time based if-condition """
|
||||
now = dt_util.now()
|
||||
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
|
||||
|
||||
if weekday is not None:
|
||||
now_weekday = WEEKDAYS[now.weekday()]
|
||||
|
||||
if isinstance(weekday, str) and weekday != now_weekday or \
|
||||
now_weekday not in weekday:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return time_if
|
||||
|
||||
|
||||
def _error_time(value, key):
|
||||
""" Helper method to print error. """
|
||||
_LOGGER.error(
|
||||
"Received invalid value for '%s': %s", key, value)
|
||||
if isinstance(value, int):
|
||||
_LOGGER.error('Make sure you wrap time values in quotes')
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
homeassistant.components.automation.zone
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Offers zone automation rules.
|
||||
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#zone-trigger
|
||||
"""
|
||||
import 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)
|
||||
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_ZONE = "zone"
|
||||
CONF_EVENT = "event"
|
||||
EVENT_ENTER = "enter"
|
||||
EVENT_LEAVE = "leave"
|
||||
DEFAULT_EVENT = EVENT_ENTER
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
if entity_id is None or zone_entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing trigger configuration key %s or %s", CONF_ENTITY_ID,
|
||||
CONF_ZONE)
|
||||
return False
|
||||
|
||||
event = config.get(CONF_EVENT, DEFAULT_EVENT)
|
||||
|
||||
def zone_automation_listener(entity, from_s, to_s):
|
||||
""" Listens 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),
|
||||
to_s.attributes.get(ATTR_LONGITUDE)):
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
if event == EVENT_ENTER and not from_match and to_match or \
|
||||
event == EVENT_LEAVE and from_match and not to_match:
|
||||
action()
|
||||
|
||||
track_state_change(
|
||||
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with zone based condition. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
if entity_id is None or zone_entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing condition configuration key %s or %s", CONF_ENTITY_ID,
|
||||
CONF_ZONE)
|
||||
return False
|
||||
|
||||
def if_in_zone():
|
||||
""" 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. """
|
||||
if not state or None in (state.attributes.get(ATTR_LATITUDE),
|
||||
state.attributes.get(ATTR_LONGITUDE)):
|
||||
return False
|
||||
|
||||
zone_state = hass.states.get(zone_entity_id)
|
||||
return zone_state and zone.in_zone(
|
||||
zone_state, state.attributes.get(ATTR_LATITUDE),
|
||||
state.attributes.get(ATTR_LONGITUDE),
|
||||
state.attributes.get(ATTR_GPS_ACCURACY, 0))
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.browser
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to launch a webbrowser on the host machine.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/browser/
|
||||
"""
|
||||
|
||||
DOMAIN = "browser"
|
||||
|
||||
@@ -4,24 +4,8 @@ homeassistant.components.camera
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with various cameras.
|
||||
|
||||
The following features are supported:
|
||||
- Returning recorded camera images and streams
|
||||
- Proxying image requests via HA for external access
|
||||
- Converting a still image url into a live video stream
|
||||
|
||||
Upcoming features
|
||||
- Recording
|
||||
- Snapshot
|
||||
- Motion Detection Recording(for supported cameras)
|
||||
- Automatic Configuration(for supported cameras)
|
||||
- Creation of child entities for supported functions
|
||||
- Collating motion event images passed via FTP into time based events
|
||||
- A service for calling camera functions
|
||||
- Camera movement(panning)
|
||||
- Zoom
|
||||
- Light/Nightvision toggling
|
||||
- Support for more devices
|
||||
- Expanded documentation
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import requests
|
||||
import logging
|
||||
@@ -103,7 +87,10 @@ def setup(hass, config):
|
||||
|
||||
if camera:
|
||||
response = camera.camera_image()
|
||||
handler.wfile.write(response)
|
||||
if response is not None:
|
||||
handler.wfile.write(response)
|
||||
else:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
else:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
|
||||
@@ -145,7 +132,8 @@ def setup(hass, config):
|
||||
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',
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Adds a Foscam IP Camera. """
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: ['username', 'password', 'ip']}, _LOGGER):
|
||||
return None
|
||||
|
||||
add_devices_callback([FoscamCamera(config)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class FoscamCamera(Camera):
|
||||
""" An implementation of a Foscam IP camera. """
|
||||
|
||||
def __init__(self, device_info):
|
||||
super(FoscamCamera, self).__init__()
|
||||
|
||||
ip_address = device_info.get('ip')
|
||||
port = device_info.get('port', 88)
|
||||
|
||||
self._base_url = 'http://' + ip_address + ':' + str(port) + '/'
|
||||
self._username = device_info.get('username')
|
||||
self._password = device_info.get('password')
|
||||
self._snap_picture_url = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \
|
||||
+ self._username + '&pwd=' + self._password
|
||||
self._name = device_info.get('name', 'Foscam Camera')
|
||||
|
||||
_LOGGER.info('Using the following URL for %s: %s',
|
||||
self._name, self._snap_picture_url)
|
||||
|
||||
def camera_image(self):
|
||||
""" 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)
|
||||
|
||||
return response.content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Return the name of this device. """
|
||||
return self._name
|
||||
@@ -3,43 +3,8 @@ homeassistant.components.camera.generic
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for IP Cameras.
|
||||
|
||||
This component provides basic support for IP cameras. For the basic support to
|
||||
work you camera must support accessing a JPEG snapshot via a URL and you will
|
||||
need to specify the "still_image_url" parameter which should be the location of
|
||||
the JPEG image.
|
||||
|
||||
As part of the basic support the following features will be provided:
|
||||
- MJPEG video streaming
|
||||
- Saving a snapshot
|
||||
- Recording(JPEG frame capture)
|
||||
|
||||
To use this component, add the following to your configuration.yaml file.
|
||||
|
||||
camera:
|
||||
platform: generic
|
||||
name: Door Camera
|
||||
username: YOUR_USERNAME
|
||||
password: YOUR_PASSWORD
|
||||
still_image_url: http://YOUR_CAMERA_IP_AND_PORT/image.jpg
|
||||
|
||||
Variables:
|
||||
|
||||
still_image_url
|
||||
*Required
|
||||
The URL your camera serves the image on, eg. http://192.168.1.21:2112/
|
||||
|
||||
name
|
||||
*Optional
|
||||
This parameter allows you to override the name of your camera in Home
|
||||
Assistant.
|
||||
|
||||
username
|
||||
*Optional
|
||||
The username for accessing your camera.
|
||||
|
||||
password
|
||||
*Optional
|
||||
The password for accessing your camera.
|
||||
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
|
||||
@@ -77,11 +42,19 @@ class GenericCamera(Camera):
|
||||
def camera_image(self):
|
||||
""" Return a still image reponse from the camera. """
|
||||
if self._username and self._password:
|
||||
response = requests.get(
|
||||
self._still_image_url,
|
||||
auth=HTTPBasicAuth(self._username, self._password))
|
||||
try:
|
||||
response = requests.get(
|
||||
self._still_image_url,
|
||||
auth=HTTPBasicAuth(self._username, self._password))
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return None
|
||||
else:
|
||||
response = requests.get(self._still_image_url)
|
||||
try:
|
||||
response = requests.get(self._still_image_url)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return None
|
||||
|
||||
return response.content
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Adds a mjpeg IP Camera. """
|
||||
if not validate_config({DOMAIN: config}, {DOMAIN: ['mjpeg_url']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
add_devices_callback([MjpegCamera(config)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class MjpegCamera(Camera):
|
||||
"""
|
||||
A generic implementation of an IP camera that is reachable over a URL.
|
||||
"""
|
||||
|
||||
def __init__(self, device_info):
|
||||
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 process_response(response):
|
||||
""" Take in a response object, return the jpg from it. """
|
||||
data = b''
|
||||
for chunk in response.iter_content(1024):
|
||||
data += chunk
|
||||
jpg_start = data.find(b'\xff\xd8')
|
||||
jpg_end = data.find(b'\xff\xd9')
|
||||
if jpg_start != -1 and jpg_end != -1:
|
||||
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)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Return the name of this device. """
|
||||
return self._name
|
||||
@@ -1,9 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.conversation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to have conversations with Home Assistant.
|
||||
This is more a proof of concept.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/conversation/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -10,14 +10,15 @@ import homeassistant.core as ha
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.const import (
|
||||
CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
|
||||
CONF_PLATFORM, ATTR_ENTITY_ID)
|
||||
|
||||
DOMAIN = "demo"
|
||||
|
||||
DEPENDENCIES = ['introduction', 'conversation']
|
||||
DEPENDENCIES = ['conversation', 'introduction', 'zone']
|
||||
|
||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'switch', 'light', 'thermostat', 'sensor', 'media_player', 'notify']
|
||||
'device_tracker', 'light', 'media_player', 'notify', 'switch', 'sensor',
|
||||
'thermostat']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@@ -33,10 +34,10 @@ def setup(hass, config):
|
||||
|
||||
# Setup sun
|
||||
if not hass.config.latitude:
|
||||
hass.config.latitude = '32.87336'
|
||||
hass.config.latitude = 32.87336
|
||||
|
||||
if not hass.config.longitude:
|
||||
hass.config.longitude = '117.22743'
|
||||
hass.config.longitude = 117.22743
|
||||
|
||||
bootstrap.setup_component(hass, 'sun')
|
||||
|
||||
@@ -60,7 +61,15 @@ def setup(hass, config):
|
||||
{'camera': {
|
||||
'platform': 'generic',
|
||||
'name': 'IP Camera',
|
||||
'still_image_url': 'http://194.218.96.92/jpg/image.jpg',
|
||||
'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',
|
||||
}})
|
||||
|
||||
# Setup scripts
|
||||
@@ -102,23 +111,6 @@ def setup(hass, config):
|
||||
}},
|
||||
]})
|
||||
|
||||
# Setup fake device tracker
|
||||
hass.states.set("device_tracker.paulus", "home",
|
||||
{ATTR_ENTITY_PICTURE:
|
||||
"http://graph.facebook.com/297400035/picture",
|
||||
ATTR_FRIENDLY_NAME: 'Paulus'})
|
||||
hass.states.set("device_tracker.anne_therese", "not_home",
|
||||
{ATTR_FRIENDLY_NAME: 'Anne Therese'})
|
||||
|
||||
hass.states.set("group.all_devices", "home",
|
||||
{
|
||||
"auto": True,
|
||||
ATTR_ENTITY_ID: [
|
||||
"device_tracker.paulus",
|
||||
"device_tracker.anne_therese"
|
||||
]
|
||||
})
|
||||
|
||||
# Setup configurator
|
||||
configurator_ids = []
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""
|
||||
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 state of the sun and devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_sun_light_trigger/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -1,52 +1,75 @@
|
||||
"""
|
||||
homeassistant.components.tracker
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
homeassistant.components.device_tracker
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides 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/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
import os
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
# pylint: disable=too-many-locals
|
||||
import csv
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.entity import _OVERWRITE
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.components import discovery, group, zone
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
|
||||
CONF_PLATFORM, DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.components import group
|
||||
ATTR_ENTITY_PICTURE, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
DOMAIN = "device_tracker"
|
||||
DEPENDENCIES = []
|
||||
|
||||
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
|
||||
DEPENDENCIES = ['zone']
|
||||
|
||||
GROUP_NAME_ALL_DEVICES = 'all devices'
|
||||
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# After how much time do we consider a device not home if
|
||||
# it does not show up on scans
|
||||
TIME_DEVICE_NOT_FOUND = timedelta(minutes=3)
|
||||
CSV_DEVICES = "known_devices.csv"
|
||||
YAML_DEVICES = 'known_devices.yaml'
|
||||
|
||||
# Filename to save known devices to
|
||||
KNOWN_DEVICES_FILE = "known_devices.csv"
|
||||
CONF_TRACK_NEW = "track_new_devices"
|
||||
DEFAULT_CONF_TRACK_NEW = True
|
||||
|
||||
CONF_SECONDS = "interval_seconds"
|
||||
CONF_CONSIDER_HOME = 'consider_home'
|
||||
DEFAULT_CONSIDER_HOME = 180 # seconds
|
||||
|
||||
DEFAULT_CONF_SECONDS = 12
|
||||
CONF_SCAN_INTERVAL = "interval_seconds"
|
||||
DEFAULT_SCAN_INTERVAL = 12
|
||||
|
||||
TRACK_NEW_DEVICES = "track_new_devices"
|
||||
CONF_AWAY_HIDE = 'hide_if_away'
|
||||
DEFAULT_AWAY_HIDE = False
|
||||
|
||||
CONF_HOME_RANGE = 'home_range'
|
||||
DEFAULT_HOME_RANGE = 100
|
||||
|
||||
SERVICE_SEE = 'see'
|
||||
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_DEV_ID = 'dev_id'
|
||||
ATTR_HOST_NAME = 'host_name'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_GPS = 'gps'
|
||||
ATTR_BATTERY = 'battery'
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
discovery.SERVICE_NETGEAR: 'netgear',
|
||||
}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Returns if any or specified device is home. """
|
||||
@@ -55,293 +78,356 @@ def is_on(hass, entity_id=None):
|
||||
return hass.states.is_state(entity, STATE_HOME)
|
||||
|
||||
|
||||
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. """
|
||||
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}
|
||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up the 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)
|
||||
|
||||
if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER):
|
||||
return False
|
||||
conf = config.get(DOMAIN, {})
|
||||
if isinstance(conf, list):
|
||||
conf = conf[0]
|
||||
consider_home = timedelta(
|
||||
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
|
||||
DEFAULT_CONSIDER_HOME))
|
||||
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
|
||||
DEFAULT_CONF_TRACK_NEW)
|
||||
home_range = util.convert(conf.get(CONF_HOME_RANGE), int,
|
||||
DEFAULT_HOME_RANGE)
|
||||
|
||||
tracker_type = config[DOMAIN].get(CONF_PLATFORM)
|
||||
devices = load_config(yaml_path, hass, consider_home, home_range)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, home_range,
|
||||
devices)
|
||||
|
||||
tracker_implementation = \
|
||||
prepare_setup_platform(hass, config, DOMAIN, tracker_type)
|
||||
|
||||
if tracker_implementation is None:
|
||||
_LOGGER.error("Unknown device_tracker type specified: %s.",
|
||||
tracker_type)
|
||||
|
||||
return False
|
||||
|
||||
device_scanner = tracker_implementation.get_scanner(hass, config)
|
||||
|
||||
if device_scanner is None:
|
||||
_LOGGER.error("Failed to initialize device scanner: %s",
|
||||
tracker_type)
|
||||
|
||||
return False
|
||||
|
||||
seconds = util.convert(config[DOMAIN].get(CONF_SECONDS), int,
|
||||
DEFAULT_CONF_SECONDS)
|
||||
|
||||
track_new_devices = config[DOMAIN].get(TRACK_NEW_DEVICES) or False
|
||||
_LOGGER.info("Tracking new devices: %s", track_new_devices)
|
||||
|
||||
tracker = DeviceTracker(hass, device_scanner, seconds, track_new_devices)
|
||||
|
||||
# We only succeeded if we got to parse the known devices file
|
||||
return not tracker.invalid_known_devices_file
|
||||
|
||||
|
||||
class DeviceTracker(object):
|
||||
""" Class that tracks which devices are home and which are not. """
|
||||
|
||||
def __init__(self, hass, device_scanner, seconds, track_new_devices):
|
||||
self.hass = hass
|
||||
|
||||
self.device_scanner = device_scanner
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# Do we track new devices by default?
|
||||
self.track_new_devices = track_new_devices
|
||||
|
||||
# Dictionary to keep track of known devices and devices we track
|
||||
self.tracked = {}
|
||||
self.untracked_devices = set()
|
||||
|
||||
# Did we encounter an invalid known devices file
|
||||
self.invalid_known_devices_file = False
|
||||
|
||||
# Wrap it in a func instead of lambda so it can be identified in
|
||||
# the bus by its __name__ attribute.
|
||||
def update_device_state(now):
|
||||
""" Triggers update of the device states. """
|
||||
self.update_devices(now)
|
||||
|
||||
dev_group = group.Group(
|
||||
hass, GROUP_NAME_ALL_DEVICES, user_defined=False)
|
||||
|
||||
def reload_known_devices_service(service):
|
||||
""" Reload known devices file. """
|
||||
self._read_known_devices_file()
|
||||
|
||||
self.update_devices(dt_util.utcnow())
|
||||
|
||||
dev_group.update_tracked_entity_ids(self.device_entity_ids)
|
||||
|
||||
reload_known_devices_service(None)
|
||||
|
||||
if self.invalid_known_devices_file:
|
||||
return
|
||||
|
||||
seconds = range(0, 60, seconds)
|
||||
|
||||
_LOGGER.info("Device tracker interval second=%s", seconds)
|
||||
track_utc_time_change(hass, update_device_state, second=seconds)
|
||||
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_DEVICE_TRACKER_RELOAD,
|
||||
reload_known_devices_service)
|
||||
|
||||
@property
|
||||
def device_entity_ids(self):
|
||||
""" Returns a set containing all device entity ids
|
||||
that are being tracked. """
|
||||
return set(device['entity_id'] for device in self.tracked.values())
|
||||
|
||||
def _update_state(self, now, device, is_home):
|
||||
""" Update the state of a device. """
|
||||
dev_info = self.tracked[device]
|
||||
|
||||
if is_home:
|
||||
# Update last seen if at home
|
||||
dev_info['last_seen'] = now
|
||||
else:
|
||||
# State remains at home if it has been seen in the last
|
||||
# TIME_DEVICE_NOT_FOUND
|
||||
is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND
|
||||
|
||||
state = STATE_HOME if is_home else STATE_NOT_HOME
|
||||
|
||||
# overwrite properties that have been set in the config file
|
||||
attr = dict(dev_info['state_attr'])
|
||||
attr.update(_OVERWRITE.get(dev_info['entity_id'], {}))
|
||||
|
||||
self.hass.states.set(
|
||||
dev_info['entity_id'], state, attr)
|
||||
|
||||
def update_devices(self, now):
|
||||
""" Update device states based on the found devices. """
|
||||
if not self.lock.acquire(False):
|
||||
def setup_platform(p_type, p_config, disc_info=None):
|
||||
""" Setup a device tracker platform. """
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
try:
|
||||
found_devices = set(dev.upper() for dev in
|
||||
self.device_scanner.scan_devices())
|
||||
if hasattr(platform, 'get_scanner'):
|
||||
scanner = platform.get_scanner(hass, {DOMAIN: p_config})
|
||||
|
||||
for device in self.tracked:
|
||||
is_home = device in found_devices
|
||||
if scanner is None:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
self._update_state(now, device, is_home)
|
||||
setup_scanner_platform(hass, p_config, scanner, tracker.see)
|
||||
return
|
||||
|
||||
if is_home:
|
||||
found_devices.remove(device)
|
||||
if not platform.setup_scanner(hass, p_config, tracker.see):
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error setting up platform %s', p_type)
|
||||
|
||||
# Did we find any devices that we didn't know about yet?
|
||||
new_devices = found_devices - self.untracked_devices
|
||||
for p_type, p_config in \
|
||||
config_per_platform(config, DOMAIN, _LOGGER):
|
||||
setup_platform(p_type, p_config)
|
||||
|
||||
if new_devices:
|
||||
if not self.track_new_devices:
|
||||
self.untracked_devices.update(new_devices)
|
||||
def device_tracker_discovered(service, info):
|
||||
""" Called when a device tracker platform is discovered. """
|
||||
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
|
||||
|
||||
self._update_known_devices_file(new_devices)
|
||||
finally:
|
||||
self.lock.release()
|
||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
|
||||
device_tracker_discovered)
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def _read_known_devices_file(self):
|
||||
""" Parse and process the known devices file. """
|
||||
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
|
||||
def update_stale(now):
|
||||
""" Clean up stale devices. """
|
||||
tracker.update_stale(now)
|
||||
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
|
||||
|
||||
# Return if no known devices file exists
|
||||
if not os.path.isfile(known_dev_path):
|
||||
tracker.setup_group()
|
||||
|
||||
def see_service(call):
|
||||
""" 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)}
|
||||
tracker.see(**args)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_SEE, see_service,
|
||||
descriptions.get(SERVICE_SEE))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DeviceTracker(object):
|
||||
""" Track devices """
|
||||
def __init__(self, hass, consider_home, track_new, home_range, devices):
|
||||
self.hass = hass
|
||||
self.devices = {dev.dev_id: dev for dev in devices}
|
||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new
|
||||
self.home_range = home_range
|
||||
self.lock = threading.Lock()
|
||||
|
||||
for device in devices:
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
|
||||
self.group = None
|
||||
|
||||
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. """
|
||||
with self.lock:
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
elif mac is not None:
|
||||
mac = mac.upper()
|
||||
device = self.mac_to_dev.get(mac)
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = str(dev_id).lower()
|
||||
device = self.devices.get(dev_id)
|
||||
|
||||
if device:
|
||||
device.seen(host_name, location_name, gps, gps_accuracy,
|
||||
battery)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
return
|
||||
|
||||
# If no device can be found, create it
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.home_range, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
device.seen(host_name, location_name, gps, gps_accuracy, battery)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group is not None:
|
||||
self.group.update_tracked_entity_ids(
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
|
||||
|
||||
def setup_group(self):
|
||||
""" Initializes 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.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
|
||||
def update_stale(self, now):
|
||||
""" Update stale devices. """
|
||||
with self.lock:
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home and
|
||||
device.stale(now)):
|
||||
device.update_ha_state(True)
|
||||
|
||||
|
||||
class Device(Entity):
|
||||
""" Tracked device. """
|
||||
|
||||
host_name = None
|
||||
location_name = None
|
||||
gps = None
|
||||
gps_accuracy = 0
|
||||
last_seen = None
|
||||
battery = None
|
||||
|
||||
# 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):
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
|
||||
# Timedelta object how long we consider a device home if it is not
|
||||
# detected anymore.
|
||||
self.consider_home = consider_home
|
||||
|
||||
# Distance in meters
|
||||
self.home_range = home_range
|
||||
# Device ID
|
||||
self.dev_id = dev_id
|
||||
self.mac = mac
|
||||
|
||||
# If we should track this device
|
||||
self.track = track
|
||||
|
||||
# Configured name
|
||||
self.config_name = name
|
||||
|
||||
# Configured picture
|
||||
self.config_picture = picture
|
||||
self.away_hide = away_hide
|
||||
|
||||
@property
|
||||
def gps_home(self):
|
||||
""" 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 self.config_name or self.host_name or DEVICE_DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" State of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Device state attributes. """
|
||||
attr = {}
|
||||
|
||||
if self.config_picture:
|
||||
attr[ATTR_ENTITY_PICTURE] = self.config_picture
|
||||
|
||||
if self.gps:
|
||||
attr[ATTR_LATITUDE] = self.gps[0]
|
||||
attr[ATTR_LONGITUDE] = self.gps[1]
|
||||
attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
|
||||
|
||||
if self.battery:
|
||||
attr[ATTR_BATTERY] = self.battery
|
||||
|
||||
return attr
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
""" 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. """
|
||||
self.last_seen = dt_util.utcnow()
|
||||
self.host_name = host_name
|
||||
self.location_name = location_name
|
||||
self.gps_accuracy = gps_accuracy or 0
|
||||
self.battery = battery
|
||||
if gps is None:
|
||||
self.gps = None
|
||||
else:
|
||||
try:
|
||||
self.gps = tuple(float(val) for val in gps)
|
||||
except ValueError:
|
||||
_LOGGER.warning('Could not parse gps value for %s: %s',
|
||||
self.dev_id, gps)
|
||||
self.gps = None
|
||||
self.update()
|
||||
|
||||
def stale(self, now=None):
|
||||
""" 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. """
|
||||
if not self.last_seen:
|
||||
return
|
||||
elif self.location_name:
|
||||
self._state = self.location_name
|
||||
elif self.gps is not None:
|
||||
zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1],
|
||||
self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
self._state = STATE_NOT_HOME
|
||||
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
||||
self._state = STATE_HOME
|
||||
else:
|
||||
self._state = zone_state.name
|
||||
|
||||
self.lock.acquire()
|
||||
elif self.stale():
|
||||
self._state = STATE_NOT_HOME
|
||||
self.last_update_home = False
|
||||
else:
|
||||
self._state = STATE_HOME
|
||||
self.last_update_home = True
|
||||
|
||||
self.untracked_devices.clear()
|
||||
|
||||
with open(known_dev_path) as inp:
|
||||
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
|
||||
|
||||
# To track which devices need an entity_id assigned
|
||||
need_entity_id = []
|
||||
|
||||
# All devices that are still in this set after we read the CSV file
|
||||
# have been removed from the file and thus need to be cleaned up.
|
||||
removed_devices = set(self.tracked.keys())
|
||||
def load_config(path, hass, consider_home, home_range):
|
||||
""" Load devices from YAML config file. """
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
return [
|
||||
Device(hass, consider_home, home_range, device.get('track', False),
|
||||
str(dev_id).lower(), str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
for dev_id, device in load_yaml_config_file(path).items()]
|
||||
|
||||
try:
|
||||
for row in csv.DictReader(inp):
|
||||
device = row['device'].upper()
|
||||
|
||||
if row['track'] == '1':
|
||||
if device in self.tracked:
|
||||
# Device exists
|
||||
removed_devices.remove(device)
|
||||
else:
|
||||
# We found a new device
|
||||
need_entity_id.append(device)
|
||||
def setup_scanner_platform(hass, config, scanner, see_device):
|
||||
""" Helper method to connect scanner-based platform to device tracker. """
|
||||
interval = util.convert(config.get(CONF_SCAN_INTERVAL), int,
|
||||
DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
self._track_device(device, row['name'])
|
||||
# Initial scan of each mac we also tell about host name for config
|
||||
seen = set()
|
||||
|
||||
# Update state_attr with latest from file
|
||||
state_attr = {
|
||||
ATTR_FRIENDLY_NAME: row['name']
|
||||
}
|
||||
def device_tracker_scan(now):
|
||||
""" Called when interval matches. """
|
||||
for mac in scanner.scan_devices():
|
||||
if mac in seen:
|
||||
host_name = None
|
||||
else:
|
||||
host_name = scanner.get_device_name(mac)
|
||||
seen.add(mac)
|
||||
see_device(mac=mac, host_name=host_name)
|
||||
|
||||
if row['picture']:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = row['picture']
|
||||
track_utc_time_change(hass, device_tracker_scan, second=range(0, 60,
|
||||
interval))
|
||||
|
||||
self.tracked[device]['state_attr'] = state_attr
|
||||
device_tracker_scan(None)
|
||||
|
||||
else:
|
||||
self.untracked_devices.add(device)
|
||||
|
||||
# Remove existing devices that we no longer track
|
||||
for device in removed_devices:
|
||||
entity_id = self.tracked[device]['entity_id']
|
||||
def update_config(path, dev_id, device):
|
||||
""" Add device to YAML config file. """
|
||||
with open(path, 'a') as out:
|
||||
out.write('\n')
|
||||
out.write('{}:\n'.format(device.dev_id))
|
||||
|
||||
_LOGGER.info("Removing entity %s", entity_id)
|
||||
|
||||
self.hass.states.remove(entity_id)
|
||||
|
||||
self.tracked.pop(device)
|
||||
|
||||
self._generate_entity_ids(need_entity_id)
|
||||
|
||||
if not self.tracked:
|
||||
_LOGGER.warning(
|
||||
"No devices to track. Please update %s.",
|
||||
known_dev_path)
|
||||
|
||||
_LOGGER.info("Loaded devices from %s", known_dev_path)
|
||||
|
||||
except KeyError:
|
||||
self.invalid_known_devices_file = True
|
||||
|
||||
_LOGGER.warning(
|
||||
("Invalid known devices file: %s. "
|
||||
"We won't update it with new found devices."),
|
||||
known_dev_path)
|
||||
|
||||
finally:
|
||||
self.lock.release()
|
||||
|
||||
def _update_known_devices_file(self, new_devices):
|
||||
""" Add new devices to known devices file. """
|
||||
if not self.invalid_known_devices_file:
|
||||
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
|
||||
|
||||
try:
|
||||
# If file does not exist we will write the header too
|
||||
is_new_file = not os.path.isfile(known_dev_path)
|
||||
|
||||
with open(known_dev_path, 'a') as outp:
|
||||
_LOGGER.info("Found %d new devices, updating %s",
|
||||
len(new_devices), known_dev_path)
|
||||
|
||||
writer = csv.writer(outp)
|
||||
|
||||
if is_new_file:
|
||||
writer.writerow(("device", "name", "track", "picture"))
|
||||
|
||||
for device in new_devices:
|
||||
# See if the device scanner knows the name
|
||||
# else defaults to unknown device
|
||||
name = self.device_scanner.get_device_name(device) or \
|
||||
DEVICE_DEFAULT_NAME
|
||||
|
||||
track = 0
|
||||
if self.track_new_devices:
|
||||
self._track_device(device, name)
|
||||
track = 1
|
||||
|
||||
writer.writerow((device, name, track, ""))
|
||||
|
||||
if self.track_new_devices:
|
||||
self._generate_entity_ids(new_devices)
|
||||
|
||||
except IOError:
|
||||
_LOGGER.exception("Error updating %s with %d new devices",
|
||||
known_dev_path, len(new_devices))
|
||||
|
||||
def _track_device(self, device, name):
|
||||
"""
|
||||
Add a device to the list of tracked devices.
|
||||
Does not generate the entity id yet.
|
||||
"""
|
||||
default_last_seen = dt_util.utcnow().replace(year=1990)
|
||||
|
||||
self.tracked[device] = {
|
||||
'name': name,
|
||||
'last_seen': default_last_seen,
|
||||
'state_attr': {ATTR_FRIENDLY_NAME: name}
|
||||
}
|
||||
|
||||
def _generate_entity_ids(self, need_entity_id):
|
||||
""" Generate entity ids for a list of devices. """
|
||||
# Setup entity_ids for the new devices
|
||||
used_entity_ids = [info['entity_id'] for device, info
|
||||
in self.tracked.items()
|
||||
if device not in need_entity_id]
|
||||
|
||||
for device in need_entity_id:
|
||||
name = self.tracked[device]['name']
|
||||
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
used_entity_ids)
|
||||
|
||||
used_entity_ids.append(entity_id)
|
||||
|
||||
self.tracked[device]['entity_id'] = entity_id
|
||||
for key, value in (('name', device.name), ('mac', device.mac),
|
||||
('picture', device.config_picture),
|
||||
('track', 'yes' if device.track else 'no'),
|
||||
(CONF_AWAY_HIDE,
|
||||
'yes' if device.away_hide else 'no')):
|
||||
out.write(' {}: {}\n'.format(key, '' if value is None else value))
|
||||
|
||||
@@ -4,41 +4,8 @@ homeassistant.components.device_tracker.actiontec
|
||||
Device tracker platform that supports scanning an Actiontec MI424WR
|
||||
(Verizon FIOS) router for device presence.
|
||||
|
||||
This device tracker needs telnet to be enabled on the router.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Actiontec tracker you will need to add something like the
|
||||
following to your configuration.yaml file. If you experience disconnects
|
||||
you can modify the home_interval variable.
|
||||
|
||||
device_tracker:
|
||||
platform: actiontec
|
||||
host: YOUR_ROUTER_IP
|
||||
username: YOUR_ADMIN_USERNAME
|
||||
password: YOUR_ADMIN_PASSWORD
|
||||
# optional:
|
||||
home_interval: 10
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your router, e.g. 192.168.1.1.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of an user with administrative privileges, usually 'admin'.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for your given admin account.
|
||||
|
||||
home_interval
|
||||
*Optional
|
||||
If the home_interval is set then the component will not let a device
|
||||
be AWAY if it has been HOME in the last home_interval minutes. This is
|
||||
in addition to the 3 minute wait built into the device_tracker component.
|
||||
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
|
||||
@@ -50,20 +17,19 @@ import telnetlib
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle, convert
|
||||
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)
|
||||
|
||||
# interval in minutes to exclude devices from a scan while they are home
|
||||
CONF_HOME_INTERVAL = "home_interval"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_LEASES_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))')
|
||||
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' +
|
||||
r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' +
|
||||
r'\ssec')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@@ -73,9 +39,7 @@ def get_scanner(hass, config):
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||
@@ -91,19 +55,11 @@ class ActiontecDeviceScanner(object):
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0)
|
||||
self.home_interval = timedelta(minutes=minutes)
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = []
|
||||
|
||||
# Test the router is accessible
|
||||
data = self.get_actiontec_data()
|
||||
self.success_init = data is not None
|
||||
_LOGGER.info("actiontec scanner initialized")
|
||||
if self.home_interval:
|
||||
_LOGGER.info("home_interval set to: %s", self.home_interval)
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
@@ -133,27 +89,13 @@ class ActiontecDeviceScanner(object):
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
exclude_targets = set()
|
||||
exclude_target_list = []
|
||||
now = dt_util.now()
|
||||
if self.home_interval:
|
||||
for host in self.last_results:
|
||||
if host.last_update + self.home_interval > now:
|
||||
exclude_targets.add(host)
|
||||
if len(exclude_targets) > 0:
|
||||
exclude_target_list = [t.ip for t in exclude_targets]
|
||||
|
||||
actiontec_data = self.get_actiontec_data()
|
||||
if not actiontec_data:
|
||||
return False
|
||||
self.last_results = []
|
||||
for client in exclude_target_list:
|
||||
if client in actiontec_data:
|
||||
actiontec_data.pop(client)
|
||||
for name, data in actiontec_data.items():
|
||||
device = Device(data['mac'], name, now)
|
||||
self.last_results.append(device)
|
||||
self.last_results.extend(exclude_targets)
|
||||
self.last_results = [Device(data['mac'], name, now)
|
||||
for name, data in actiontec_data.items()
|
||||
if data['timevalid'] > -60]
|
||||
_LOGGER.info("actiontec scan successful")
|
||||
return True
|
||||
|
||||
@@ -186,6 +128,7 @@ class ActiontecDeviceScanner(object):
|
||||
if match is not None:
|
||||
devices[match.group('ip')] = {
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper()
|
||||
'mac': match.group('mac').upper(),
|
||||
'timevalid': int(match.group('timevalid'))
|
||||
}
|
||||
return devices
|
||||
|
||||
@@ -4,33 +4,8 @@ homeassistant.components.device_tracker.aruba
|
||||
Device tracker platform that supports scanning a Aruba Access Point for device
|
||||
presence.
|
||||
|
||||
This device tracker needs telnet to be enabled on the router.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Aruba tracker you will need to add something like the following
|
||||
to your configuration.yaml file. You also need to enable Telnet in the
|
||||
configuration page of your router.
|
||||
|
||||
device_tracker:
|
||||
platform: aruba
|
||||
host: YOUR_ACCESS_POINT_IP
|
||||
username: YOUR_ADMIN_USERNAME
|
||||
password: YOUR_ADMIN_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your router, e.g. 192.168.1.1.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of an user with administrative privileges, usually 'admin'.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for your given admin account.
|
||||
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
|
||||
|
||||
@@ -4,32 +4,8 @@ homeassistant.components.device_tracker.asuswrt
|
||||
Device tracker platform that supports scanning a ASUSWRT router for device
|
||||
presence.
|
||||
|
||||
This device tracker needs telnet to be enabled on the router.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the ASUSWRT tracker you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
device_tracker:
|
||||
platform: asuswrt
|
||||
host: YOUR_ROUTER_IP
|
||||
username: YOUR_ADMIN_USERNAME
|
||||
password: YOUR_ADMIN_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your router, e.g. 192.168.1.1.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of an user with administrative privileges, usually 'admin'.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for your given admin account.
|
||||
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
|
||||
@@ -157,15 +133,30 @@ class AsusWrtDeviceScanner(object):
|
||||
devices = {}
|
||||
for lease in leases_result:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
if not match:
|
||||
_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
|
||||
host = match.group('host')
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
'host': match.group('host'),
|
||||
'status': ''
|
||||
}
|
||||
|
||||
for neighbor in neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
|
||||
continue
|
||||
if match.group('ip') in devices:
|
||||
devices[match.group('ip')]['status'] = match.group('status')
|
||||
return devices
|
||||
|
||||
@@ -4,30 +4,8 @@ homeassistant.components.device_tracker.ddwrt
|
||||
Device tracker platform that supports scanning a DD-WRT router for device
|
||||
presence.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the DD-WRT tracker you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
device_tracker:
|
||||
platform: ddwrt
|
||||
host: YOUR_ROUTER_IP
|
||||
username: YOUR_ADMIN_USERNAME
|
||||
password: YOUR_ADMIN_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your router, e.g. 192.168.1.1.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of an user with administrative privileges, usually 'admin'.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for your given admin account.
|
||||
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
|
||||
@@ -46,6 +24,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
|
||||
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@@ -77,7 +56,7 @@ class DdWrtDeviceScanner(object):
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.mac2name = None
|
||||
self.mac2name = {}
|
||||
|
||||
# Test the router is accessible
|
||||
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||
@@ -98,30 +77,33 @@ class DdWrtDeviceScanner(object):
|
||||
|
||||
with self.lock:
|
||||
# if not initialised and not already scanned and not found
|
||||
if self.mac2name is None or device not in self.mac2name:
|
||||
if device not in self.mac2name:
|
||||
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
|
||||
if not data:
|
||||
return
|
||||
return None
|
||||
|
||||
dhcp_leases = data.get('dhcp_leases', None)
|
||||
if dhcp_leases:
|
||||
# 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
|
||||
# every 5 elements represents one hosts, the MAC
|
||||
# is the third element and the name is the first
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
mac = elements[mac_index]
|
||||
self.mac2name[mac] = elements[idx * 5]
|
||||
|
||||
return self.mac2name.get(device, None)
|
||||
if not dhcp_leases:
|
||||
return None
|
||||
|
||||
# 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
|
||||
# every 5 elements represents one hosts, the MAC
|
||||
# is the third element and the name is the first
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
mac = elements[mac_index]
|
||||
self.mac2name[mac] = elements[idx * 5]
|
||||
|
||||
return self.mac2name.get(device)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
@@ -141,29 +123,25 @@ class DdWrtDeviceScanner(object):
|
||||
if not data:
|
||||
return False
|
||||
|
||||
if data:
|
||||
self.last_results = []
|
||||
active_clients = data.get('active_wireless', None)
|
||||
if active_clients:
|
||||
# This is really lame, instead of using JSON the DD-WRT UI
|
||||
# uses its own data format for some reason and then
|
||||
# regex's out values so I guess I have to do the same,
|
||||
# LAME!!!
|
||||
self.last_results = []
|
||||
|
||||
# remove leading and trailing single quotes
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
active_clients = data.get('active_wireless', None)
|
||||
if not active_clients:
|
||||
return False
|
||||
|
||||
num_clients = int(len(elements)/9)
|
||||
for idx in range(0, num_clients):
|
||||
# get every 9th element which is the MAC address
|
||||
index = idx * 9
|
||||
if index < len(elements):
|
||||
self.last_results.append(elements[index])
|
||||
# This is really lame, instead of using JSON the DD-WRT UI
|
||||
# uses its own data format for some reason and then
|
||||
# regex's out values so I guess I have to do the same,
|
||||
# LAME!!!
|
||||
|
||||
return True
|
||||
# remove leading and trailing single quotes
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
|
||||
return False
|
||||
self.last_results.extend(item for item in elements
|
||||
if _MAC_REGEX.match(item))
|
||||
|
||||
return True
|
||||
|
||||
def get_ddwrt_data(self, url):
|
||||
""" Retrieve data from DD-WRT and return parsed result. """
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Demo platform for the device tracker.
|
||||
|
||||
device_tracker:
|
||||
platform: demo
|
||||
"""
|
||||
import random
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a demo tracker. """
|
||||
|
||||
def offset():
|
||||
""" Return random offset. """
|
||||
return (random.randrange(500, 2000)) / 2e5 * random.choice((-1, 1))
|
||||
|
||||
def random_see(dev_id, name):
|
||||
""" Randomize a sighting. """
|
||||
see(
|
||||
dev_id=dev_id,
|
||||
host_name=name,
|
||||
gps=(hass.config.latitude + offset(),
|
||||
hass.config.longitude + offset()),
|
||||
gps_accuracy=random.randrange(50, 150),
|
||||
battery=random.randrange(10, 90)
|
||||
)
|
||||
|
||||
def observe(call=None):
|
||||
""" Observe three entities. """
|
||||
random_see('demo_paulus', 'Paulus')
|
||||
random_see('demo_anne_therese', 'Anne Therese')
|
||||
|
||||
observe()
|
||||
|
||||
see(
|
||||
dev_id='demo_home_boy',
|
||||
host_name='Home Boy',
|
||||
gps=[hass.config.latitude - 0.00002, hass.config.longitude + 0.00002],
|
||||
gps_accuracy=20,
|
||||
battery=53
|
||||
)
|
||||
|
||||
hass.services.register(DOMAIN, 'demo', observe)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
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")
|
||||
@@ -4,33 +4,8 @@ homeassistant.components.device_tracker.luci
|
||||
Device tracker platform that supports scanning a OpenWRT router for device
|
||||
presence.
|
||||
|
||||
It's required that the luci RPC package is installed on the OpenWRT router:
|
||||
# opkg install luci-mod-rpc
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Luci tracker you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
device_tracker:
|
||||
platform: luci
|
||||
host: YOUR_ROUTER_IP
|
||||
username: YOUR_ADMIN_USERNAME
|
||||
password: YOUR_ADMIN_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your router, e.g. 192.168.1.1.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of an user with administrative privileges, usually 'admin'.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for your given admin account.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.luci/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
MQTT platform for the device tracker.
|
||||
|
||||
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
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_QOS = 'qos'
|
||||
CONF_DEVICES = 'devices'
|
||||
|
||||
DEFAULT_QOS = 0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a MQTT tracker. """
|
||||
devices = config.get(CONF_DEVICES)
|
||||
qos = util.convert(config.get(CONF_QOS), int, DEFAULT_QOS)
|
||||
|
||||
if not isinstance(devices, dict):
|
||||
_LOGGER.error('Expected %s to be a dict, found %s', CONF_DEVICES,
|
||||
devices)
|
||||
return False
|
||||
|
||||
dev_id_lookup = {}
|
||||
|
||||
def device_tracker_message_received(topic, payload, qos):
|
||||
""" MQTT message received. """
|
||||
see(dev_id=dev_id_lookup[topic], location_name=payload)
|
||||
|
||||
for dev_id, topic in devices.items():
|
||||
dev_id_lookup[topic] = dev_id
|
||||
mqtt.subscribe(hass, topic, device_tracker_message_received, qos)
|
||||
|
||||
return True
|
||||
@@ -4,30 +4,8 @@ homeassistant.components.device_tracker.netgear
|
||||
Device tracker platform that supports scanning a Netgear router for device
|
||||
presence.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Netgear tracker you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
device_tracker:
|
||||
platform: netgear
|
||||
host: YOUR_ROUTER_IP
|
||||
username: YOUR_ADMIN_USERNAME
|
||||
password: YOUR_ADMIN_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your router, e.g. 192.168.1.1.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of an user with administrative privileges, usually 'admin'.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for your given admin account.
|
||||
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
|
||||
@@ -70,7 +48,6 @@ class NetgearDeviceScanner(object):
|
||||
self.lock = threading.Lock()
|
||||
|
||||
if host is None:
|
||||
print("BIER")
|
||||
self._api = pynetgear.Netgear()
|
||||
elif username is None:
|
||||
self._api = pynetgear.Netgear(password, host)
|
||||
|
||||
@@ -3,26 +3,8 @@ homeassistant.components.device_tracker.nmap
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a network with nmap.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the nmap tracker you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
device_tracker:
|
||||
platform: nmap_tracker
|
||||
hosts: 192.168.1.1/24
|
||||
|
||||
Variables:
|
||||
|
||||
hosts
|
||||
*Required
|
||||
The IP addresses to scan in the network-prefix notation (192.168.1.1/24) or
|
||||
the range notation (192.168.1.1-255).
|
||||
|
||||
home_interval
|
||||
*Optional
|
||||
Number of minutes it will not scan devices that it found in previous results.
|
||||
This is to save battery.
|
||||
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
|
||||
@@ -44,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# interval in minutes to exclude devices from a scan while they are home
|
||||
CONF_HOME_INTERVAL = "home_interval"
|
||||
|
||||
REQUIREMENTS = ['python-nmap==0.4.1']
|
||||
REQUIREMENTS = ['python-nmap==0.4.3']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
@@ -117,15 +99,18 @@ class NmapDeviceScanner(object):
|
||||
scanner = PortScanner()
|
||||
|
||||
options = "-F --host-timeout 5"
|
||||
exclude_targets = set()
|
||||
|
||||
if self.home_interval:
|
||||
now = dt_util.now()
|
||||
for host in self.last_results:
|
||||
if host.last_update + self.home_interval > now:
|
||||
exclude_targets.add(host)
|
||||
if len(exclude_targets) > 0:
|
||||
target_list = [t.ip for t in exclude_targets]
|
||||
options += " --exclude {}".format(",".join(target_list))
|
||||
boundary = dt_util.now() - self.home_interval
|
||||
last_results = [device for device in self.last_results
|
||||
if device.last_update > boundary]
|
||||
if last_results:
|
||||
# Pylint is confused here.
|
||||
# pylint: disable=no-member
|
||||
options += " --exclude {}".format(",".join(device.ip for device
|
||||
in last_results))
|
||||
else:
|
||||
last_results = []
|
||||
|
||||
try:
|
||||
result = scanner.scan(hosts=self.hosts, arguments=options)
|
||||
@@ -133,18 +118,17 @@ class NmapDeviceScanner(object):
|
||||
return False
|
||||
|
||||
now = dt_util.now()
|
||||
self.last_results = []
|
||||
for ipv4, info in result['scan'].items():
|
||||
if info['status']['state'] != 'up':
|
||||
continue
|
||||
name = info['hostnames'][0] if info['hostnames'] else ipv4
|
||||
name = info['hostnames'][0]['name'] if info['hostnames'] else ipv4
|
||||
# Mac address only returned if nmap ran as root
|
||||
mac = info['addresses'].get('mac') or _arp(ipv4)
|
||||
if mac is None:
|
||||
continue
|
||||
device = Device(mac.upper(), name, ipv4, now)
|
||||
self.last_results.append(device)
|
||||
self.last_results.extend(exclude_targets)
|
||||
last_results.append(Device(mac.upper(), name, ipv4, now))
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info("nmap scan successful")
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.owntracks
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
OwnTracks platform for the device tracker.
|
||||
|
||||
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 homeassistant.components.mqtt as mqtt
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a OwnTracksks tracker. """
|
||||
|
||||
def owntracks_location_update(topic, payload, qos):
|
||||
""" 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(
|
||||
'Unable to parse payload as JSON: %s', payload)
|
||||
return
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != 'location':
|
||||
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']
|
||||
|
||||
see(**kwargs)
|
||||
|
||||
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.snmp
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports 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
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pysnmp==4.2.5']
|
||||
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_BASEOID = "baseoid"
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns an snmp scanner """
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_COMMUNITY, CONF_BASEOID]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = SnmpScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SnmpScanner(object):
|
||||
"""
|
||||
This class queries any SNMP capable Acces Point for connected devices.
|
||||
"""
|
||||
def __init__(self, config):
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
self.snmp = cmdgen.CommandGenerator()
|
||||
|
||||
self.host = cmdgen.UdpTransportTarget((config[CONF_HOST], 161))
|
||||
self.community = cmdgen.CommunityData(config[CONF_COMMUNITY])
|
||||
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = []
|
||||
|
||||
# Test the router is accessible
|
||||
data = self.get_snmp_data()
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""
|
||||
Scans for new devices and return a list containing found device IDs.
|
||||
"""
|
||||
|
||||
self._update_info()
|
||||
return [client['mac'] for client in self.last_results]
|
||||
|
||||
# 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. """
|
||||
# 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.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
data = self.get_snmp_data()
|
||||
if not data:
|
||||
return False
|
||||
|
||||
self.last_results = data
|
||||
return True
|
||||
|
||||
def get_snmp_data(self):
|
||||
""" Fetch mac addresses from WAP via SNMP. """
|
||||
|
||||
devices = []
|
||||
|
||||
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
|
||||
self.community, self.host, self.baseoid)
|
||||
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
return
|
||||
if errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
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')
|
||||
mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
|
||||
devices.append({'mac': mac})
|
||||
return devices
|
||||
@@ -4,32 +4,8 @@ homeassistant.components.device_tracker.thomson
|
||||
Device tracker platform that supports scanning a THOMSON router for device
|
||||
presence.
|
||||
|
||||
This device tracker needs telnet to be enabled on the router.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the THOMSON tracker you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
device_tracker:
|
||||
platform: thomson
|
||||
host: YOUR_ROUTER_IP
|
||||
username: YOUR_ADMIN_USERNAME
|
||||
password: YOUR_ADMIN_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your router, e.g. 192.168.1.1.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of an user with administrative privileges, usually 'admin'.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for your given admin account.
|
||||
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
|
||||
|
||||
@@ -4,36 +4,8 @@ homeassistant.components.device_tracker.tomato
|
||||
Device tracker platform that supports scanning a Tomato router for device
|
||||
presence.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Tomato tracker you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
device_tracker:
|
||||
platform: tomato
|
||||
host: YOUR_ROUTER_IP
|
||||
username: YOUR_ADMIN_USERNAME
|
||||
password: YOUR_ADMIN_PASSWORD
|
||||
http_id: ABCDEFG
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your router, e.g. 192.168.1.1.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of an user with administrative privileges, usually 'admin'.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for your given admin account.
|
||||
|
||||
http_id
|
||||
*Required
|
||||
The value can be obtained by logging in to the Tomato admin interface and
|
||||
search for http_id in the page source code.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.tomato/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
@@ -4,30 +4,8 @@ homeassistant.components.device_tracker.tplink
|
||||
Device tracker platform that supports scanning a TP-Link router for device
|
||||
presence.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the TP-Link tracker you will need to add something like the following
|
||||
to your configuration.yaml file.
|
||||
|
||||
device_tracker:
|
||||
platform: tplink
|
||||
host: YOUR_ROUTER_IP
|
||||
username: YOUR_ADMIN_USERNAME
|
||||
password: YOUR_ADMIN_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The IP address of your router, e.g. 192.168.1.1.
|
||||
|
||||
username
|
||||
*Required
|
||||
The username of an user with administrative privileges, usually 'admin'.
|
||||
|
||||
password
|
||||
*Required
|
||||
The password for your given admin account.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.tplink/
|
||||
"""
|
||||
import base64
|
||||
import logging
|
||||
@@ -54,10 +32,13 @@ def get_scanner(hass, config):
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = Tplink2DeviceScanner(config[DOMAIN])
|
||||
scanner = Tplink3DeviceScanner(config[DOMAIN])
|
||||
|
||||
if not scanner.success_init:
|
||||
scanner = TplinkDeviceScanner(config[DOMAIN])
|
||||
scanner = Tplink2DeviceScanner(config[DOMAIN])
|
||||
|
||||
if not scanner.success_init:
|
||||
scanner = TplinkDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
@@ -156,7 +137,7 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
|
||||
with self.lock:
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
url = 'http://{}/data/map_access_wireless_client_grid.json'\
|
||||
url = 'http://{}/data/map_access_wireless_client_grid.json' \
|
||||
.format(self.host)
|
||||
referer = 'http://{}'.format(self.host)
|
||||
|
||||
@@ -166,7 +147,7 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
|
||||
b64_encoded_username_password = base64.b64encode(
|
||||
username_password.encode('ascii')
|
||||
).decode('ascii')
|
||||
cookie = 'Authorization=Basic {}'\
|
||||
cookie = 'Authorization=Basic {}' \
|
||||
.format(b64_encoded_username_password)
|
||||
|
||||
response = requests.post(url, headers={'referer': referer,
|
||||
@@ -183,7 +164,119 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
|
||||
self.last_results = {
|
||||
device['mac_addr'].replace('-', ':'): device['name']
|
||||
for device in result
|
||||
}
|
||||
}
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
"""
|
||||
This class queries the Archer C9 router running version 150811 or higher
|
||||
of TP-Link firmware for connected devices.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
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.
|
||||
"""
|
||||
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
|
||||
_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
|
||||
|
||||
response = requests.post(url,
|
||||
params={'operation': 'login',
|
||||
'username': self.username,
|
||||
'password': self.password},
|
||||
headers={'referer': referer})
|
||||
|
||||
try:
|
||||
self.stok = response.json().get('data').get('stok')
|
||||
_LOGGER.info(self.stok)
|
||||
regex_result = re.search('sysauth=(.*);',
|
||||
response.headers['set-cookie'])
|
||||
self.sysauth = regex_result.group(1)
|
||||
_LOGGER.info(self.sysauth)
|
||||
return True
|
||||
except ValueError:
|
||||
_LOGGER.error("Couldn't fetch auth tokens!")
|
||||
return False
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
||||
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)
|
||||
referer = 'http://{}/webpages/index.html'.format(self.host)
|
||||
|
||||
response = requests.post(url,
|
||||
params={'operation': 'load'},
|
||||
headers={'referer': referer},
|
||||
cookies={'sysauth': self.sysauth})
|
||||
|
||||
try:
|
||||
json_response = response.json()
|
||||
|
||||
if json_response.get('success'):
|
||||
result = response.json().get('data')
|
||||
else:
|
||||
if json_response.get('errorcode') == 'timeout':
|
||||
_LOGGER.info("Token timed out. "
|
||||
"Relogging on next scan.")
|
||||
self.stok = ''
|
||||
self.sysauth = ''
|
||||
return False
|
||||
else:
|
||||
_LOGGER.error("An unknown error happened "
|
||||
"while fetching data.")
|
||||
return False
|
||||
except ValueError:
|
||||
_LOGGER.error("Router didn't respond with JSON. "
|
||||
"Check if credentials are correct.")
|
||||
return False
|
||||
|
||||
if result:
|
||||
self.last_results = {
|
||||
device['mac'].replace('-', ':'): device['mac']
|
||||
for device in result
|
||||
}
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
homeassistant.components.device_tracker.ubus
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Device tracker platform that supports scanning a OpenWRT router for device
|
||||
presence.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.ubus/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import threading
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
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)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Luci scanner. """
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = UbusDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
self.lock = threading.Lock()
|
||||
self.last_results = {}
|
||||
self.url = 'http://{}/ubus'.format(host)
|
||||
|
||||
self.session_id = _get_session_id(self.url, username, password)
|
||||
self.hostapd = []
|
||||
self.leasefile = None
|
||||
self.mac2name = None
|
||||
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.
|
||||
"""
|
||||
|
||||
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. """
|
||||
|
||||
with self.lock:
|
||||
if self.leasefile is None:
|
||||
result = _req_json_rpc(self.url, self.session_id,
|
||||
'call', 'uci', 'get',
|
||||
config="dhcp", type="dnsmasq")
|
||||
if result:
|
||||
values = result["values"].values()
|
||||
self.leasefile = next(iter(values))["leasefile"]
|
||||
else:
|
||||
return
|
||||
|
||||
if self.mac2name is None:
|
||||
result = _req_json_rpc(self.url, self.session_id,
|
||||
'call', 'file', 'read',
|
||||
path=self.leasefile)
|
||||
if result:
|
||||
self.mac2name = dict()
|
||||
for line in result["data"].splitlines():
|
||||
hosts = line.split(" ")
|
||||
self.mac2name[hosts[1].upper()] = hosts[3]
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
|
||||
return self.mac2name.get(device.upper(), None)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensures the information from the Luci router is up to date.
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Checking ARP")
|
||||
|
||||
if not self.hostapd:
|
||||
hostapd = _req_json_rpc(self.url, self.session_id,
|
||||
'list', 'hostapd.*', '')
|
||||
self.hostapd.extend(hostapd.keys())
|
||||
|
||||
self.last_results = []
|
||||
results = 0
|
||||
for hostapd in self.hostapd:
|
||||
result = _req_json_rpc(self.url, self.session_id,
|
||||
'call', hostapd, 'get_clients')
|
||||
|
||||
if result:
|
||||
results = results + 1
|
||||
self.last_results.extend(result['clients'].keys())
|
||||
|
||||
return bool(results)
|
||||
|
||||
|
||||
def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
|
||||
""" Perform one JSON RPC operation. """
|
||||
|
||||
data = json.dumps({"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": rpcmethod,
|
||||
"params": [session_id,
|
||||
subsystem,
|
||||
method,
|
||||
params]})
|
||||
|
||||
try:
|
||||
res = requests.post(url, data=data, timeout=5)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return
|
||||
|
||||
if res.status_code == 200:
|
||||
response = res.json()
|
||||
|
||||
if rpcmethod == "call":
|
||||
return response["result"][1]
|
||||
else:
|
||||
return response["result"]
|
||||
|
||||
|
||||
def _get_session_id(url, username, password):
|
||||
""" Get authentication token for the given host+username+password. """
|
||||
res = _req_json_rpc(url, "00000000000000000000000000000000", 'call',
|
||||
'session', 'login', username=username,
|
||||
password=password)
|
||||
return res["ubus_rpc_session"]
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
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.
|
||||
@@ -19,22 +18,24 @@ from homeassistant.const import (
|
||||
|
||||
DOMAIN = "discovery"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['netdisco==0.3']
|
||||
REQUIREMENTS = ['netdisco==0.5.1']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
# Next 3 lines for now a mirror from netdisco.const
|
||||
# Should setup a mapping netdisco.const -> own constants
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_CAST = 'google_cast'
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
SERVICE_SONOS = 'sonos'
|
||||
SERVICE_PLEX = 'plex_mediaserver'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_WEMO: "switch",
|
||||
SERVICE_CAST: "media_player",
|
||||
SERVICE_HUE: "light",
|
||||
SERVICE_NETGEAR: 'device_tracker',
|
||||
SERVICE_SONOS: 'media_player',
|
||||
SERVICE_PLEX: 'media_player',
|
||||
}
|
||||
|
||||
|
||||
@@ -79,13 +80,6 @@ def setup(hass, config):
|
||||
if not component:
|
||||
return
|
||||
|
||||
# Hack - fix when device_tracker supports discovery
|
||||
if service == SERVICE_NETGEAR:
|
||||
bootstrap.setup_component(hass, component, {
|
||||
'device_tracker': {'platform': 'netgear'}
|
||||
})
|
||||
return
|
||||
|
||||
# This component cannot be setup.
|
||||
if not bootstrap.setup_component(hass, component, config):
|
||||
return
|
||||
@@ -95,6 +89,7 @@ def setup(hass, config):
|
||||
ATTR_DISCOVERED: info
|
||||
})
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def start_discovery(event):
|
||||
""" Start discovering. """
|
||||
netdisco = DiscoveryService(SCAN_INTERVAL)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.downloader
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to download files.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/downloader/
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
@@ -42,6 +44,10 @@ def setup(hass, config):
|
||||
|
||||
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
|
||||
|
||||
# If path is relative, we assume relative to HASS config dir
|
||||
if not os.path.isabs(download_path):
|
||||
download_path = hass.config.path(download_path)
|
||||
|
||||
if not os.path.isdir(download_path):
|
||||
|
||||
logger.error(
|
||||
|
||||
@@ -8,9 +8,10 @@ import re
|
||||
import os
|
||||
import logging
|
||||
|
||||
from . import version
|
||||
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
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
@@ -19,11 +20,13 @@ INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
FRONTEND_URLS = [
|
||||
URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent']
|
||||
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent', '/devInfo']
|
||||
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup serving the frontend. """
|
||||
@@ -43,6 +46,9 @@ def setup(hass, config):
|
||||
hass.http.register_path(
|
||||
'HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_static, False)
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/local/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_local, False)
|
||||
|
||||
return True
|
||||
|
||||
@@ -68,6 +74,7 @@ def _handle_get_root(handler, path_match, data):
|
||||
|
||||
template_html = template_html.replace('{{ app_url }}', app_url)
|
||||
template_html = template_html.replace('{{ auth }}', auth)
|
||||
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
|
||||
|
||||
handler.wfile.write(template_html.encode("UTF-8"))
|
||||
|
||||
@@ -76,10 +83,24 @@ def _handle_get_static(handler, path_match, data):
|
||||
""" Returns a static file for the frontend. """
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
|
||||
# Strip md5 hash out of frontend filename
|
||||
if re.match(r'^frontend-[A-Za-z0-9]{32}\.html$', req_file):
|
||||
req_file = "frontend.html"
|
||||
# Strip md5 hash out
|
||||
fingerprinted = _FINGERPRINT.match(req_file)
|
||||
if fingerprinted:
|
||||
req_file = "{}.{}".format(*fingerprinted.groups())
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
|
||||
|
||||
handler.write_file(path)
|
||||
|
||||
|
||||
def _handle_get_local(handler, path_match, data):
|
||||
"""
|
||||
Returns 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
|
||||
|
||||
handler.write_file(path)
|
||||
|
||||
@@ -46,6 +46,6 @@
|
||||
</div>
|
||||
<script src='/static/webcomponents-lite.min.js'></script>
|
||||
<link rel='import' href='/static/{{ app_url }}' />
|
||||
<home-assistant auth='{{ auth }}'></home-assistant>
|
||||
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by update_mdi script """
|
||||
VERSION = "38EF63D0474411E4B3CF842B2B6CFE1B"
|
||||
@@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "35ecb5457a9ff0f4142c2605b53eb843"
|
||||
VERSION = "b75e3c9ebd3de2dae0912a89499127a9"
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 797 B |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
homeassistant.components.group
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to group devices that can be turned on or off.
|
||||
"""
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/group/
|
||||
"""
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.helpers import generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
@@ -12,7 +13,8 @@ from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
|
||||
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN)
|
||||
STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
DOMAIN = "group"
|
||||
DEPENDENCIES = []
|
||||
@@ -22,7 +24,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
ATTR_AUTO = "auto"
|
||||
|
||||
# List of ON/OFF state tuples for groupable states
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
|
||||
(STATE_OPEN, STATE_CLOSED)]
|
||||
|
||||
|
||||
def _get_group_on_off(state):
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.history
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provide pre-made queries on top of the recorder component.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/history/
|
||||
"""
|
||||
import re
|
||||
from datetime import timedelta
|
||||
@@ -147,8 +149,6 @@ def _api_history_period(handler, path_match, data):
|
||||
|
||||
end_time = start_time + one_day
|
||||
|
||||
print("Fetchign", start_time, end_time)
|
||||
|
||||
entity_id = data.get('filter_entity_id')
|
||||
|
||||
handler.write_json(
|
||||
|
||||
@@ -1,76 +1,11 @@
|
||||
"""
|
||||
homeassistant.components.httpinterface
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
homeassistant.components.http
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
This module provides an API and a HTTP interface for debug purposes.
|
||||
|
||||
By default it will run on port 8123.
|
||||
|
||||
All API calls have to be accompanied by an 'api_password' parameter and will
|
||||
return JSON. If successful calls will return status code 200 or 201.
|
||||
|
||||
Other status codes that can occur are:
|
||||
- 400 (Bad Request)
|
||||
- 401 (Unauthorized)
|
||||
- 404 (Not Found)
|
||||
- 405 (Method not allowed)
|
||||
|
||||
The api supports the following actions:
|
||||
|
||||
/api - GET
|
||||
Returns message if API is up and running.
|
||||
Example result:
|
||||
{
|
||||
"message": "API running."
|
||||
}
|
||||
|
||||
/api/states - GET
|
||||
Returns a list of entities for which a state is available
|
||||
Example result:
|
||||
[
|
||||
{ .. state object .. },
|
||||
{ .. state object .. }
|
||||
]
|
||||
|
||||
/api/states/<entity_id> - GET
|
||||
Returns the current state from an entity
|
||||
Example result:
|
||||
{
|
||||
"attributes": {
|
||||
"next_rising": "07:04:15 29-10-2013",
|
||||
"next_setting": "18:00:31 29-10-2013"
|
||||
},
|
||||
"entity_id": "weather.sun",
|
||||
"last_changed": "23:24:33 28-10-2013",
|
||||
"state": "below_horizon"
|
||||
}
|
||||
|
||||
/api/states/<entity_id> - POST
|
||||
Updates the current state of an entity. Returns status code 201 if successful
|
||||
with location header of updated resource and as body the new state.
|
||||
parameter: new_state - string
|
||||
optional parameter: attributes - JSON encoded object
|
||||
Example result:
|
||||
{
|
||||
"attributes": {
|
||||
"next_rising": "07:04:15 29-10-2013",
|
||||
"next_setting": "18:00:31 29-10-2013"
|
||||
},
|
||||
"entity_id": "weather.sun",
|
||||
"last_changed": "23:24:33 28-10-2013",
|
||||
"state": "below_horizon"
|
||||
}
|
||||
|
||||
/api/events/<event_type> - POST
|
||||
Fires an event with event_type
|
||||
optional parameter: event_data - JSON encoded object
|
||||
Example result:
|
||||
{
|
||||
"message": "Event download_file fired."
|
||||
}
|
||||
|
||||
For more details about the RESTful API, please refer to the documentation at
|
||||
https://home-assistant.io/developers/api/
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
import logging
|
||||
@@ -205,7 +140,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
self.serve_forever()
|
||||
|
||||
def register_path(self, method, url, callback, require_auth=True):
|
||||
""" Registers a path wit the server. """
|
||||
""" Registers a path with the server. """
|
||||
self.paths.append((method, url, callback, require_auth))
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
@@ -232,7 +167,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
def log_message(self, fmt, *arguments):
|
||||
""" Redirect built-in log to HA logging """
|
||||
_LOGGER.info(fmt, *arguments)
|
||||
if self.server.no_password_set:
|
||||
_LOGGER.info(fmt, *arguments)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
fmt, *(arg.replace(self.server.api_password, '*******')
|
||||
if isinstance(arg, str) else arg for arg in arguments))
|
||||
|
||||
def _handle_request(self, method): # pylint: disable=too-many-branches
|
||||
""" Does some common checks and calls appropriate method. """
|
||||
@@ -487,7 +427,7 @@ class ServerSession:
|
||||
return self._expiry < date_util.utcnow()
|
||||
|
||||
|
||||
class SessionStore:
|
||||
class SessionStore(object):
|
||||
""" Responsible for storing and retrieving http sessions """
|
||||
def __init__(self, enabled=True):
|
||||
""" Set up the session store """
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.ifttt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
This component enable you to trigger Maker IFTTT recipes.
|
||||
Check https://ifttt.com/maker for details.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use Maker IFTTT you will need to add something like the following to your
|
||||
config/configuration.yaml.
|
||||
|
||||
ifttt:
|
||||
key: xxxxx-x-xxxxxxxxxxxxx
|
||||
|
||||
Variables:
|
||||
|
||||
key
|
||||
*Required
|
||||
Your api key
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ifttt/
|
||||
"""
|
||||
import logging
|
||||
import requests
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.introduction
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Component that will help guide the user taking its first steps.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/introduction/
|
||||
"""
|
||||
import logging
|
||||
|
||||
@@ -24,13 +26,13 @@ def setup(hass, config=None):
|
||||
Here are some resources to get started:
|
||||
|
||||
- Configuring Home Assistant:
|
||||
https://home-assistant.io/getting-started/configuration.html
|
||||
https://home-assistant.io/getting-started/configuration/
|
||||
|
||||
- Available components:
|
||||
https://home-assistant.io/components/
|
||||
|
||||
- Troubleshooting your configuration:
|
||||
https://home-assistant.io/getting-started/troubleshooting-configuration.html
|
||||
https://home-assistant.io/getting-started/troubleshooting-configuration/
|
||||
|
||||
- Getting help:
|
||||
https://home-assistant.io/help/
|
||||
|
||||
@@ -5,7 +5,7 @@ Connects to an ISY-994 controller and loads relevant components to control its
|
||||
devices. Also contains the base classes for ISY Sensors, Lights, and Switches.
|
||||
|
||||
For configuration details please visit the documentation for this component at
|
||||
https://home-assistant.io/components/isy994.html
|
||||
https://home-assistant.io/components/isy994/
|
||||
"""
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.keyboard
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to emulate keyboard presses on host machine.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/keyboard/
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
@@ -1,65 +1,23 @@
|
||||
"""
|
||||
homeassistant.components.light
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to interact with lights.
|
||||
|
||||
It offers the following services:
|
||||
|
||||
TURN_OFF - Turns one or multiple lights off.
|
||||
|
||||
Supports following parameters:
|
||||
- transition
|
||||
Integer that represents the time the light should take to transition to
|
||||
the new state.
|
||||
- entity_id
|
||||
String or list of strings that point at entity_ids of lights.
|
||||
|
||||
TURN_ON - Turns one or multiple lights on and change attributes.
|
||||
|
||||
Supports following parameters:
|
||||
- transition
|
||||
Integer that represents the time the light should take to transition to
|
||||
the new state.
|
||||
|
||||
- entity_id
|
||||
String or list of strings that point at entity_ids of lights.
|
||||
|
||||
- profile
|
||||
String with the name of one of the built-in profiles (relax, energize,
|
||||
concentrate, reading) or one of the custom profiles defined in
|
||||
light_profiles.csv in the current working directory.
|
||||
|
||||
Light profiles define a xy color and a brightness.
|
||||
|
||||
If a profile is given and a brightness or xy color then the profile values
|
||||
will be overwritten.
|
||||
|
||||
- xy_color
|
||||
A list containing two floats representing the xy color you want the light
|
||||
to be.
|
||||
|
||||
- rgb_color
|
||||
A list containing three integers representing the xy color you want the
|
||||
light to be.
|
||||
|
||||
- brightness
|
||||
Integer between 0 and 255 representing how bright you want the light to be.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/light/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import csv
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.components import group, discovery, wink, isy994, zwave
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.components import group, discovery, wink, isy994
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
|
||||
DOMAIN = "light"
|
||||
@@ -77,6 +35,7 @@ ATTR_TRANSITION = "transition"
|
||||
# lists holding color values
|
||||
ATTR_RGB_COLOR = "rgb_color"
|
||||
ATTR_XY_COLOR = "xy_color"
|
||||
ATTR_COLOR_TEMP = "color_temp"
|
||||
|
||||
# int with value 0 .. 255 representing brightness of the light
|
||||
ATTR_BRIGHTNESS = "brightness"
|
||||
@@ -92,6 +51,7 @@ FLASH_LONG = "long"
|
||||
# Apply an effect to the light, can be EFFECT_COLORLOOP
|
||||
ATTR_EFFECT = "effect"
|
||||
EFFECT_COLORLOOP = "colorloop"
|
||||
EFFECT_WHITE = "white"
|
||||
|
||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||
|
||||
@@ -100,11 +60,14 @@ DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_LIGHTS: 'wink',
|
||||
isy994.DISCOVER_LIGHTS: 'isy994',
|
||||
discovery.SERVICE_HUE: 'hue',
|
||||
zwave.DISCOVER_LIGHTS: 'zwave',
|
||||
}
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
'brightness': ATTR_BRIGHTNESS,
|
||||
'color_xy': ATTR_XY_COLOR,
|
||||
'color_temp': ATTR_COLOR_TEMP,
|
||||
'rgb_color': ATTR_RGB_COLOR,
|
||||
'xy_color': ATTR_XY_COLOR,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -119,8 +82,8 @@ def is_on(hass, entity_id=None):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||
rgb_color=None, xy_color=None, profile=None, flash=None,
|
||||
effect=None):
|
||||
rgb_color=None, xy_color=None, color_temp=None, profile=None,
|
||||
flash=None, effect=None):
|
||||
""" Turns all or specified light on. """
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
@@ -130,6 +93,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||
(ATTR_BRIGHTNESS, brightness),
|
||||
(ATTR_RGB_COLOR, rgb_color),
|
||||
(ATTR_XY_COLOR, xy_color),
|
||||
(ATTR_COLOR_TEMP, color_temp),
|
||||
(ATTR_FLASH, flash),
|
||||
(ATTR_EFFECT, effect),
|
||||
] if value is not None
|
||||
@@ -150,7 +114,7 @@ def turn_off(hass, entity_id=None, transition=None):
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-locals
|
||||
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
|
||||
def setup(hass, config):
|
||||
""" Exposes light control via statemachine and services. """
|
||||
|
||||
@@ -240,32 +204,33 @@ def setup(hass, config):
|
||||
# ValueError if value could not be converted to float
|
||||
pass
|
||||
|
||||
if ATTR_COLOR_TEMP in dat:
|
||||
# color_temp should be an int of mirads value
|
||||
colortemp = dat.get(ATTR_COLOR_TEMP)
|
||||
|
||||
# Without this check, a ctcolor with value '99' would work
|
||||
# These values are based on Philips Hue, may need ajustment later
|
||||
if isinstance(colortemp, int) and 154 <= colortemp <= 500:
|
||||
params[ATTR_COLOR_TEMP] = colortemp
|
||||
|
||||
if ATTR_RGB_COLOR in dat:
|
||||
try:
|
||||
# rgb_color should be a list containing 3 ints
|
||||
rgb_color = dat.get(ATTR_RGB_COLOR)
|
||||
|
||||
if len(rgb_color) == 3:
|
||||
params[ATTR_XY_COLOR] = \
|
||||
color_util.color_RGB_to_xy(int(rgb_color[0]),
|
||||
int(rgb_color[1]),
|
||||
int(rgb_color[2]))
|
||||
params[ATTR_RGB_COLOR] = [int(val) for val in rgb_color]
|
||||
|
||||
except (TypeError, ValueError):
|
||||
# TypeError if rgb_color is not iterable
|
||||
# ValueError if not all values can be converted to int
|
||||
pass
|
||||
|
||||
if ATTR_FLASH in dat:
|
||||
if dat[ATTR_FLASH] == FLASH_SHORT:
|
||||
params[ATTR_FLASH] = FLASH_SHORT
|
||||
if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG):
|
||||
params[ATTR_FLASH] = dat[ATTR_FLASH]
|
||||
|
||||
elif dat[ATTR_FLASH] == FLASH_LONG:
|
||||
params[ATTR_FLASH] = FLASH_LONG
|
||||
|
||||
if ATTR_EFFECT in dat:
|
||||
if dat[ATTR_EFFECT] == EFFECT_COLORLOOP:
|
||||
params[ATTR_EFFECT] = EFFECT_COLORLOOP
|
||||
if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE):
|
||||
params[ATTR_EFFECT] = dat[ATTR_EFFECT]
|
||||
|
||||
for light in target_lights:
|
||||
light.turn_on(**params)
|
||||
@@ -275,11 +240,13 @@ def setup(hass, config):
|
||||
light.update_ha_state(True)
|
||||
|
||||
# Listen for light on and light off service calls
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON,
|
||||
handle_light_service)
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service,
|
||||
descriptions.get(SERVICE_TURN_ON))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF,
|
||||
handle_light_service)
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
|
||||
descriptions.get(SERVICE_TURN_OFF))
|
||||
|
||||
return True
|
||||
|
||||
@@ -294,10 +261,20 @@ class Light(ToggleEntity):
|
||||
return None
|
||||
|
||||
@property
|
||||
def color_xy(self):
|
||||
def xy_color(self):
|
||||
""" XY color value [float, float]. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
""" RGB color value [int, int, int] """
|
||||
return None
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
""" CT color value in mirads. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
@@ -314,6 +291,12 @@ class Light(ToggleEntity):
|
||||
if value:
|
||||
data[attr] = value
|
||||
|
||||
if ATTR_RGB_COLOR not in data and ATTR_XY_COLOR in data and \
|
||||
ATTR_BRIGHTNESS in data:
|
||||
data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB(
|
||||
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
|
||||
data[ATTR_BRIGHTNESS])
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr is not None:
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
homeassistant.components.light.blinksticklight
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Blinkstick lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.blinksticklight/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from blinkstick import blinkstick
|
||||
|
||||
from homeassistant.components.light import (Light, ATTR_RGB_COLOR)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIREMENTS = ["blinkstick==1.1.7"]
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Add device specified by serial number. """
|
||||
stick = blinkstick.find_by_serial(config['serial'])
|
||||
|
||||
add_devices_callback([BlinkStickLight(stick, config['name'])])
|
||||
|
||||
|
||||
class BlinkStickLight(Light):
|
||||
""" Represents a BlinkStick light. """
|
||||
|
||||
def __init__(self, stick, name):
|
||||
self._stick = stick
|
||||
self._name = name
|
||||
self._serial = stick.get_serial()
|
||||
self._rgb_color = stick.get_color()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Polling needed. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the light. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
""" Read back the color of the light. """
|
||||
return self._rgb_color
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" Check whether any of the LEDs colors are non-zero. """
|
||||
return sum(self._rgb_color) > 0
|
||||
|
||||
def update(self):
|
||||
""" Read back the device state """
|
||||
self._rgb_color = self._stick.get_color()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
||||
else:
|
||||
self._rgb_color = [255, 255, 255]
|
||||
|
||||
self._stick.set_color(red=self._rgb_color[0],
|
||||
green=self._rgb_color[1],
|
||||
blue=self._rgb_color[2])
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off """
|
||||
self._stick.turn_off()
|
||||
@@ -1,37 +1,40 @@
|
||||
"""
|
||||
homeassistant.components.light.demo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Demo platform that implements lights.
|
||||
|
||||
"""
|
||||
import random
|
||||
|
||||
from homeassistant.components.light import (
|
||||
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR)
|
||||
Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP)
|
||||
|
||||
|
||||
LIGHT_COLORS = [
|
||||
[0.368, 0.180],
|
||||
[0.460, 0.470],
|
||||
[237, 224, 33],
|
||||
[255, 63, 111],
|
||||
]
|
||||
|
||||
LIGHT_TEMPS = [240, 380]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Find and return demo lights. """
|
||||
add_devices_callback([
|
||||
DemoLight("Bed Light", False),
|
||||
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0]),
|
||||
DemoLight("Kitchen Lights", True, LIGHT_COLORS[1])
|
||||
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
|
||||
DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0])
|
||||
])
|
||||
|
||||
|
||||
class DemoLight(Light):
|
||||
""" Provides a demo switch. """
|
||||
def __init__(self, name, state, xy=None, brightness=180):
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, state, rgb=None, ct=None, brightness=180):
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._xy = xy or random.choice(LIGHT_COLORS)
|
||||
self._rgb = rgb or random.choice(LIGHT_COLORS)
|
||||
self._ct = ct or random.choice(LIGHT_TEMPS)
|
||||
self._brightness = brightness
|
||||
|
||||
@property
|
||||
@@ -50,9 +53,14 @@ class DemoLight(Light):
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def color_xy(self):
|
||||
""" XY color value. """
|
||||
return self._xy
|
||||
def rgb_color(self):
|
||||
""" rgb color value. """
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
""" CT color temperature. """
|
||||
return self._ct
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -63,8 +71,11 @@ class DemoLight(Light):
|
||||
""" Turn the device on. """
|
||||
self._state = True
|
||||
|
||||
if ATTR_XY_COLOR in kwargs:
|
||||
self._xy = kwargs[ATTR_XY_COLOR]
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
self._ct = kwargs[ATTR_COLOR_TEMP]
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
@@ -2,19 +2,25 @@
|
||||
homeassistant.components.light.hue
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Hue lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.hue/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
|
||||
from homeassistant.components.light import (
|
||||
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
|
||||
ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_EFFECT,
|
||||
EFFECT_COLORLOOP)
|
||||
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP,
|
||||
ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT,
|
||||
ATTR_EFFECT, EFFECT_COLORLOOP, ATTR_RGB_COLOR)
|
||||
|
||||
REQUIREMENTS = ['phue==0.8']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
@@ -28,21 +34,37 @@ _CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _find_host_from_config(hass):
|
||||
""" Attempt to detect host based on existing configuration. """
|
||||
path = hass.config.path(PHUE_CONFIG_FILE)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(path) as inp:
|
||||
return next(json.loads(''.join(inp)).keys().__iter__())
|
||||
except (ValueError, AttributeError, StopIteration):
|
||||
# ValueError if can't parse as JSON
|
||||
# AttributeError if JSON value is not a dict
|
||||
# StopIteration if no keys
|
||||
return None
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Gets the Hue lights. """
|
||||
try:
|
||||
# pylint: disable=unused-variable
|
||||
import phue # noqa
|
||||
except ImportError:
|
||||
_LOGGER.exception("Error while importing dependency phue.")
|
||||
|
||||
return
|
||||
|
||||
if discovery_info is not None:
|
||||
host = urlparse(discovery_info[1]).hostname
|
||||
else:
|
||||
host = config.get(CONF_HOST, None)
|
||||
|
||||
if host is None:
|
||||
host = _find_host_from_config(hass)
|
||||
|
||||
if host is None:
|
||||
_LOGGER.error('No host found in configuration')
|
||||
return False
|
||||
|
||||
# Only act if we are not already configuring this host
|
||||
if host in _CONFIGURING:
|
||||
return
|
||||
@@ -123,6 +145,7 @@ def request_configuration(host, hass, add_devices_callback):
|
||||
|
||||
return
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def hue_configuration_callback(data):
|
||||
""" Actions to do when our configuration callback is called. """
|
||||
setup_bridge(host, hass, add_devices_callback)
|
||||
@@ -162,10 +185,15 @@ class HueLight(Light):
|
||||
return self.info['state']['bri']
|
||||
|
||||
@property
|
||||
def color_xy(self):
|
||||
def xy_color(self):
|
||||
""" XY color value. """
|
||||
return self.info['state'].get('xy')
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
""" CT color value. """
|
||||
return self.info['state'].get('ct')
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
@@ -187,6 +215,12 @@ class HueLight(Light):
|
||||
|
||||
if ATTR_XY_COLOR in kwargs:
|
||||
command['xy'] = kwargs[ATTR_XY_COLOR]
|
||||
elif ATTR_RGB_COLOR in kwargs:
|
||||
command['xy'] = color_util.color_RGB_to_xy(
|
||||
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
command['ct'] = kwargs[ATTR_COLOR_TEMP]
|
||||
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
homeassistant.components.light.hyperion
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Hyperion remotes.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.hyperion/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
import json
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.components.light import (Light, ATTR_RGB_COLOR)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Sets up a Hyperion server remote """
|
||||
host = config.get(CONF_HOST, None)
|
||||
port = config.get("port", 19444)
|
||||
device = Hyperion(host, port)
|
||||
if device.setup():
|
||||
add_devices_callback([device])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class Hyperion(Light):
|
||||
""" Represents a Hyperion remote """
|
||||
|
||||
def __init__(self, host, port):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._name = host
|
||||
self._is_available = True
|
||||
self._rgb_color = [255, 255, 255]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Return the hostname of the server. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
""" Last RGB color value set. """
|
||||
return self._rgb_color
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the device is online. """
|
||||
return self._is_available
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the lights on. """
|
||||
if self._is_available:
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
||||
|
||||
self.json_request({"command": "color", "priority": 128,
|
||||
"color": self._rgb_color})
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Disconnect the remote. """
|
||||
self.json_request({"command": "clearall"})
|
||||
|
||||
def update(self):
|
||||
""" Ping the remote. """
|
||||
# just see if the remote port is open
|
||||
self._is_available = self.json_request()
|
||||
|
||||
def setup(self):
|
||||
""" Get the hostname of the remote. """
|
||||
response = self.json_request({"command": "serverinfo"})
|
||||
if response:
|
||||
self._name = response["info"]["hostname"]
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def json_request(self, request=None, wait_for_response=False):
|
||||
""" Communicate with the json server. """
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
|
||||
try:
|
||||
sock.connect((self._host, self._port))
|
||||
except OSError:
|
||||
sock.close()
|
||||
return False
|
||||
|
||||
if not request:
|
||||
# no communication needed, simple presence detection returns True
|
||||
sock.close()
|
||||
return True
|
||||
|
||||
sock.send(bytearray(json.dumps(request) + "\n", "utf-8"))
|
||||
try:
|
||||
buf = sock.recv(4096)
|
||||
except socket.timeout:
|
||||
# something is wrong, assume it's offline
|
||||
sock.close()
|
||||
return False
|
||||
|
||||
# read until a newline or timeout
|
||||
buffering = True
|
||||
while buffering:
|
||||
if "\n" in str(buf, "utf-8"):
|
||||
response = str(buf, "utf-8").split("\n")[0]
|
||||
buffering = False
|
||||
else:
|
||||
try:
|
||||
more = sock.recv(4096)
|
||||
except socket.timeout:
|
||||
more = None
|
||||
if not more:
|
||||
buffering = False
|
||||
response = str(buf, "utf-8")
|
||||
else:
|
||||
buf += more
|
||||
|
||||
sock.close()
|
||||
return json.loads(response)
|
||||
@@ -2,6 +2,9 @@
|
||||
homeassistant.components.light.isy994
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for ISY994 lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/isy994/
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
@@ -1,51 +1,74 @@
|
||||
"""
|
||||
homeassistant.components.light.limitlessled
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for LimitlessLED bulbs.
|
||||
|
||||
Support for LimitlessLED bulbs, also known as...
|
||||
|
||||
- EasyBulb
|
||||
- AppLight
|
||||
- AppLamp
|
||||
- MiLight
|
||||
- LEDme
|
||||
- dekolight
|
||||
- iLight
|
||||
|
||||
Configuration:
|
||||
|
||||
To use limitlessled you will need to add the following to your
|
||||
configuration.yaml file.
|
||||
|
||||
light:
|
||||
platform: limitlessled
|
||||
host: 192.168.1.10
|
||||
group_1_name: Living Room
|
||||
group_2_name: Bedroom
|
||||
group_3_name: Office
|
||||
group_4_name: Kitchen
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.limitlessled/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
from homeassistant.components.light import (Light, ATTR_BRIGHTNESS,
|
||||
ATTR_XY_COLOR)
|
||||
from homeassistant.util.color import color_RGB_to_xy
|
||||
ATTR_RGB_COLOR, ATTR_EFFECT,
|
||||
EFFECT_COLORLOOP, EFFECT_WHITE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['ledcontroller==1.0.7']
|
||||
REQUIREMENTS = ['ledcontroller==1.1.0']
|
||||
|
||||
COLOR_TABLE = {
|
||||
'white': [0xFF, 0xFF, 0xFF],
|
||||
'violet': [0xEE, 0x82, 0xEE],
|
||||
'royal_blue': [0x41, 0x69, 0xE1],
|
||||
'baby_blue': [0x87, 0xCE, 0xFA],
|
||||
'aqua': [0x00, 0xFF, 0xFF],
|
||||
'royal_mint': [0x7F, 0xFF, 0xD4],
|
||||
'seafoam_green': [0x2E, 0x8B, 0x57],
|
||||
'green': [0x00, 0x80, 0x00],
|
||||
'lime_green': [0x32, 0xCD, 0x32],
|
||||
'yellow': [0xFF, 0xFF, 0x00],
|
||||
'yellow_orange': [0xDA, 0xA5, 0x20],
|
||||
'orange': [0xFF, 0xA5, 0x00],
|
||||
'red': [0xFF, 0x00, 0x00],
|
||||
'pink': [0xFF, 0xC0, 0xCB],
|
||||
'fusia': [0xFF, 0x00, 0xFF],
|
||||
'lilac': [0xDA, 0x70, 0xD6],
|
||||
'lavendar': [0xE6, 0xE6, 0xFA],
|
||||
}
|
||||
|
||||
|
||||
def _distance_squared(rgb1, rgb2):
|
||||
""" Return sum of squared distances of each color part. """
|
||||
return sum((val1-val2)**2 for val1, val2 in zip(rgb1, rgb2))
|
||||
|
||||
|
||||
def _rgb_to_led_color(rgb_color):
|
||||
""" Convert an RGB color to the closest color string and color. """
|
||||
return sorted((_distance_squared(rgb_color, color), name)
|
||||
for name, color in COLOR_TABLE.items())[0][1]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Gets the LimitlessLED lights. """
|
||||
import ledcontroller
|
||||
|
||||
led = ledcontroller.LedController(config['host'])
|
||||
# Handle old configuration format:
|
||||
bridges = config.get('bridges', [config])
|
||||
|
||||
for bridge_id, bridge in enumerate(bridges):
|
||||
bridge['id'] = bridge_id
|
||||
|
||||
pool = ledcontroller.LedControllerPool([x['host'] for x in bridges])
|
||||
|
||||
lights = []
|
||||
for i in range(1, 5):
|
||||
if 'group_%d_name' % (i) in config:
|
||||
lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
|
||||
for bridge in bridges:
|
||||
for i in range(1, 5):
|
||||
name_key = 'group_%d_name' % i
|
||||
if name_key in bridge:
|
||||
group_type = bridge.get('group_%d_type' % i, 'rgbw')
|
||||
lights.append(LimitlessLED.factory(pool, bridge['id'], i,
|
||||
bridge[name_key],
|
||||
group_type))
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
@@ -53,43 +76,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
class LimitlessLED(Light):
|
||||
""" Represents a LimitlessLED light """
|
||||
|
||||
def __init__(self, led, group, name):
|
||||
self.led = led
|
||||
@staticmethod
|
||||
def factory(pool, controller_id, group, name, group_type):
|
||||
''' Construct a Limitless LED of the appropriate type '''
|
||||
if group_type == 'white':
|
||||
return WhiteLimitlessLED(pool, controller_id, group, name)
|
||||
elif group_type == 'rgbw':
|
||||
return RGBWLimitlessLED(pool, controller_id, group, name)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, pool, controller_id, group, name, group_type):
|
||||
self.pool = pool
|
||||
self.controller_id = controller_id
|
||||
self.group = group
|
||||
|
||||
self.pool.execute(self.controller_id, "set_group_type", self.group,
|
||||
group_type)
|
||||
|
||||
# LimitlessLEDs don't report state, we have track it ourselves.
|
||||
self.led.off(self.group)
|
||||
self.pool.execute(self.controller_id, "off", self.group)
|
||||
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._state = False
|
||||
self._brightness = 100
|
||||
self._xy_color = color_RGB_to_xy(255, 255, 255)
|
||||
|
||||
# Build a color table that maps an RGB color to a color string
|
||||
# recognized by LedController's set_color method
|
||||
self._color_table = [(color_RGB_to_xy(*x[0]), x[1]) for x in [
|
||||
((0xFF, 0xFF, 0xFF), 'white'),
|
||||
((0xEE, 0x82, 0xEE), 'violet'),
|
||||
((0x41, 0x69, 0xE1), 'royal_blue'),
|
||||
((0x87, 0xCE, 0xFA), 'baby_blue'),
|
||||
((0x00, 0xFF, 0xFF), 'aqua'),
|
||||
((0x7F, 0xFF, 0xD4), 'royal_mint'),
|
||||
((0x2E, 0x8B, 0x57), 'seafoam_green'),
|
||||
((0x00, 0x80, 0x00), 'green'),
|
||||
((0x32, 0xCD, 0x32), 'lime_green'),
|
||||
((0xFF, 0xFF, 0x00), 'yellow'),
|
||||
((0xDA, 0xA5, 0x20), 'yellow_orange'),
|
||||
((0xFF, 0xA5, 0x00), 'orange'),
|
||||
((0xFF, 0x00, 0x00), 'red'),
|
||||
((0xFF, 0xC0, 0xCB), 'pink'),
|
||||
((0xFF, 0x00, 0xFF), 'fusia'),
|
||||
((0xDA, 0x70, 0xD6), 'lilac'),
|
||||
((0xE6, 0xE6, 0xFA), 'lavendar'),
|
||||
]]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a demo light. """
|
||||
""" No polling needed. """
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -97,30 +109,34 @@ class LimitlessLED(Light):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
self._state = False
|
||||
self.pool.execute(self.controller_id, "off", self.group)
|
||||
self.update_ha_state()
|
||||
|
||||
|
||||
class RGBWLimitlessLED(LimitlessLED):
|
||||
""" Represents a RGBW LimitlessLED light """
|
||||
|
||||
def __init__(self, pool, controller_id, group, name):
|
||||
super().__init__(pool, controller_id, group, name, 'rgbw')
|
||||
|
||||
self._brightness = 100
|
||||
self._led_color = 'white'
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def color_xy(self):
|
||||
return self._xy_color
|
||||
|
||||
def _xy_to_led_color(self, xy_color):
|
||||
""" Convert an XY color to the closest LedController color string. """
|
||||
def abs_dist_squared(p_0, p_1):
|
||||
""" Returns the absolute value of the squared distance """
|
||||
return abs((p_0[0] - p_1[0])**2 + (p_0[1] - p_1[1])**2)
|
||||
|
||||
candidates = [(abs_dist_squared(xy_color, x[0]), x[1]) for x in
|
||||
self._color_table]
|
||||
|
||||
# First candidate in the sorted list is closest to desired color:
|
||||
return sorted(candidates)[0][1]
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
def rgb_color(self):
|
||||
return COLOR_TABLE[self._led_color]
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
@@ -129,15 +145,34 @@ class LimitlessLED(Light):
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
if ATTR_XY_COLOR in kwargs:
|
||||
self._xy_color = kwargs[ATTR_XY_COLOR]
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._led_color = _rgb_to_led_color(kwargs[ATTR_RGB_COLOR])
|
||||
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
|
||||
if effect == EFFECT_COLORLOOP:
|
||||
self.pool.execute(self.controller_id, "disco", self.group)
|
||||
elif effect == EFFECT_WHITE:
|
||||
self.pool.execute(self.controller_id, "white", self.group)
|
||||
else:
|
||||
self.pool.execute(self.controller_id, "set_color",
|
||||
self._led_color, self.group)
|
||||
|
||||
# Brightness can be set independently of color
|
||||
self.pool.execute(self.controller_id, "set_brightness",
|
||||
self._brightness / 255.0, self.group)
|
||||
|
||||
self.led.set_color(self._xy_to_led_color(self._xy_color), self.group)
|
||||
self.led.set_brightness(self._brightness / 255.0, self.group)
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
self._state = False
|
||||
self.led.off(self.group)
|
||||
|
||||
class WhiteLimitlessLED(LimitlessLED):
|
||||
""" Represents a White LimitlessLED light """
|
||||
|
||||
def __init__(self, pool, controller_id, group, name):
|
||||
super().__init__(pool, controller_id, group, name, 'white')
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
self._state = True
|
||||
self.pool.execute(self.controller_id, "on", self.group)
|
||||
self.update_ha_state()
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
homeassistant.components.light.mqtt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure a MQTT light.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.mqtt/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util.color as color_util
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.light import (Light,
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "MQTT Light"
|
||||
DEFAULT_QOS = 0
|
||||
DEFAULT_PAYLOAD_ON = "on"
|
||||
DEFAULT_PAYLOAD_OFF = "off"
|
||||
DEFAULT_RGB_PATTERN = "%d,%d,%d"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Add MQTT Light. """
|
||||
|
||||
if config.get('command_topic') is None:
|
||||
_LOGGER.error("Missing required variable: command_topic")
|
||||
return False
|
||||
|
||||
add_devices_callback([MqttLight(
|
||||
hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
{"state_topic": config.get('state_topic'),
|
||||
"command_topic": config.get('command_topic'),
|
||||
"brightness_state_topic": config.get('brightness_state_topic'),
|
||||
"brightness_command_topic":
|
||||
config.get('brightness_command_topic'),
|
||||
"rgb_state_topic": config.get('rgb_state_topic'),
|
||||
"rgb_command_topic": config.get('rgb_command_topic')},
|
||||
config.get('rgb', None),
|
||||
config.get('qos', DEFAULT_QOS),
|
||||
{"on": config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||
"off": config.get('payload_off', DEFAULT_PAYLOAD_OFF)},
|
||||
config.get('brightness'),
|
||||
config.get('optimistic', DEFAULT_OPTIMISTIC))])
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
|
||||
class MqttLight(Light):
|
||||
""" Provides a MQTT light. """
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name,
|
||||
topic,
|
||||
rgb, qos,
|
||||
payload,
|
||||
brightness, optimistic):
|
||||
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._rgb = rgb
|
||||
self._qos = qos
|
||||
self._payload = payload
|
||||
self._brightness = brightness
|
||||
self._optimistic = optimistic
|
||||
self._state = False
|
||||
self._xy = None
|
||||
|
||||
def message_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
if payload == self._payload["on"]:
|
||||
self._state = True
|
||||
elif payload == self._payload["off"]:
|
||||
self._state = False
|
||||
|
||||
self.update_ha_state()
|
||||
|
||||
if self._topic["state_topic"] is None:
|
||||
# force optimistic mode
|
||||
self._optimistic = True
|
||||
else:
|
||||
# Subscribe the state_topic
|
||||
mqtt.subscribe(self._hass, self._topic["state_topic"],
|
||||
message_received, self._qos)
|
||||
|
||||
def brightness_received(topic, payload, qos):
|
||||
""" A new MQTT message for the brightness has been received. """
|
||||
self._brightness = int(payload)
|
||||
self.update_ha_state()
|
||||
|
||||
def rgb_received(topic, payload, qos):
|
||||
""" A new MQTT message has been received. """
|
||||
self._rgb = [int(val) for val in payload.split(',')]
|
||||
self._xy = color_util.color_RGB_to_xy(int(self._rgb[0]),
|
||||
int(self._rgb[1]),
|
||||
int(self._rgb[2]))
|
||||
self.update_ha_state()
|
||||
|
||||
if self._topic["brightness_state_topic"] is not None:
|
||||
mqtt.subscribe(self._hass, self._topic["brightness_state_topic"],
|
||||
brightness_received, self._qos)
|
||||
self._brightness = 255
|
||||
else:
|
||||
self._brightness = None
|
||||
|
||||
if self._topic["rgb_state_topic"] is not None:
|
||||
mqtt.subscribe(self._hass, self._topic["rgb_state_topic"],
|
||||
rgb_received, self._qos)
|
||||
self._xy = [0, 0]
|
||||
else:
|
||||
self._xy = None
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
""" Brightness of this light between 0..255. """
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
""" RGB color value. """
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def color_xy(self):
|
||||
""" RGB color value. """
|
||||
return self._xy
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a MQTT light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs and \
|
||||
self._topic["rgb_command_topic"] is not None:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
rgb = DEFAULT_RGB_PATTERN % tuple(self._rgb)
|
||||
mqtt.publish(self._hass, self._topic["rgb_command_topic"],
|
||||
rgb, self._qos)
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs and \
|
||||
self._topic["brightness_command_topic"] is not None:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
mqtt.publish(self._hass, self._topic["brightness_command_topic"],
|
||||
self._brightness, self._qos)
|
||||
|
||||
mqtt.publish(self._hass, self._topic["command_topic"],
|
||||
self._payload["on"], self._qos)
|
||||
|
||||
if self._optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
mqtt.publish(self._hass, self._topic["command_topic"],
|
||||
self._payload["off"], self._qos)
|
||||
|
||||
if self._optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
homeassistant.components.light.rfxtrx
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for RFXtrx lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.rfxtrx/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
import RFXtrx as rfxtrxmod
|
||||
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Setup the RFXtrx platform. """
|
||||
lights = []
|
||||
devices = config.get('devices', None)
|
||||
if devices:
|
||||
for entity_id, entity_info in devices.items():
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
_LOGGER.info("Add %s rfxtrx.light", entity_info['name'])
|
||||
rfxobject = rfxtrx.get_rfx_object(entity_info['packetid'])
|
||||
new_light = RfxtrxLight(entity_info['name'], rfxobject, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_light
|
||||
lights.append(new_light)
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
def light_update(event):
|
||||
""" Callback for light updates from the RFXtrx gateway. """
|
||||
if not isinstance(event.device, rfxtrxmod.LightingDevice):
|
||||
return
|
||||
|
||||
# Add entity if not exist and the automatic_add is True
|
||||
entity_id = slugify(event.device.id_string.lower())
|
||||
if entity_id not in rfxtrx.RFX_DEVICES:
|
||||
automatic_add = config.get('automatic_add', False)
|
||||
if not automatic_add:
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Automatic add %s rfxtrx.light (Class: %s Sub: %s)",
|
||||
entity_id,
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype
|
||||
)
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
entity_name = "%s : %s" % (entity_id, pkt_id)
|
||||
new_light = RfxtrxLight(entity_name, event, False)
|
||||
rfxtrx.RFX_DEVICES[entity_id] = new_light
|
||||
add_devices_callback([new_light])
|
||||
|
||||
# Check if entity exists or previously added automatically
|
||||
if entity_id in rfxtrx.RFX_DEVICES:
|
||||
_LOGGER.debug(
|
||||
"EntityID: %s light_update. Command: %s",
|
||||
entity_id,
|
||||
event.values['Command']
|
||||
)
|
||||
if event.values['Command'] == 'On'\
|
||||
or event.values['Command'] == 'Off':
|
||||
if event.values['Command'] == 'On':
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_on()
|
||||
else:
|
||||
rfxtrx.RFX_DEVICES[entity_id].turn_off()
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update)
|
||||
|
||||
|
||||
class RfxtrxLight(Light):
|
||||
""" Provides a RFXtrx light. """
|
||||
def __init__(self, name, event, state):
|
||||
self._name = name
|
||||
self._event = event
|
||||
self._state = state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the light if any. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if light is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the light on. """
|
||||
|
||||
if hasattr(self, '_event') and self._event:
|
||||
self._event.device.send_on(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the light off. """
|
||||
|
||||
if hasattr(self, '_event') and self._event:
|
||||
self._event.device.send_off(rfxtrx.RFXOBJECT.transport)
|
||||
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
@@ -0,0 +1,56 @@
|
||||
# Describes the format for available light services
|
||||
|
||||
turn_on:
|
||||
description: Turn a light on
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to turn on
|
||||
example: 'light.kitchen'
|
||||
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to next state
|
||||
example: 60
|
||||
|
||||
rgb_color:
|
||||
description: Color for the light in RGB-format
|
||||
example: '[255, 100, 100]'
|
||||
|
||||
xy_color:
|
||||
description: Color for the light in XY-format
|
||||
example: '[0.52, 0.43]'
|
||||
|
||||
color_temp:
|
||||
description: Color temperature for the light in mireds (154-500)
|
||||
example: '250'
|
||||
|
||||
brightness:
|
||||
description: Number between 0..255 indicating brightness
|
||||
example: 120
|
||||
|
||||
profile:
|
||||
description: Name of a light profile to use
|
||||
example: relax
|
||||
|
||||
flash:
|
||||
description: If the light should flash
|
||||
values:
|
||||
- short
|
||||
- long
|
||||
|
||||
effect:
|
||||
description: Light effect
|
||||
values:
|
||||
- colorloop
|
||||
|
||||
turn_off:
|
||||
description: Turn a light off
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to turn off
|
||||
example: 'light.kitchen'
|
||||
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to next state
|
||||
example: 60
|
||||
@@ -2,16 +2,21 @@
|
||||
homeassistant.components.light.tellstick
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Tellstick lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.tellstick/
|
||||
"""
|
||||
import logging
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_FRIENDLY_NAME)
|
||||
import tellcore.constants as tellcore_constants
|
||||
|
||||
REQUIREMENTS = ['tellcore-py==1.0.4']
|
||||
from tellcore.library import DirectCallbackDispatcher
|
||||
REQUIREMENTS = ['tellcore-py==1.1.2']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Find and return Tellstick lights. """
|
||||
|
||||
@@ -22,13 +27,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"Failed to import tellcore")
|
||||
return []
|
||||
|
||||
core = telldus.TelldusCore()
|
||||
core = telldus.TelldusCore(callback_dispatcher=DirectCallbackDispatcher())
|
||||
|
||||
switches_and_lights = core.devices()
|
||||
lights = []
|
||||
|
||||
for switch in switches_and_lights:
|
||||
if switch.methods(tellcore_constants.TELLSTICK_DIM):
|
||||
lights.append(TellstickLight(switch))
|
||||
|
||||
def _device_event_callback(id_, method, data, cid):
|
||||
""" Called from the TelldusCore library to update one device """
|
||||
for light_device in lights:
|
||||
if light_device.tellstick_device.id == id_:
|
||||
# Execute the update in another thread
|
||||
light_device.update_ha_state(True)
|
||||
break
|
||||
|
||||
callback_id = core.register_device_event(_device_event_callback)
|
||||
|
||||
def unload_telldus_lib(event):
|
||||
""" Un-register the callback bindings """
|
||||
if callback_id is not None:
|
||||
core.unregister_callback(callback_id)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, unload_telldus_lib)
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
|
||||
@@ -40,15 +64,15 @@ class TellstickLight(Light):
|
||||
tellcore_constants.TELLSTICK_UP |
|
||||
tellcore_constants.TELLSTICK_DOWN)
|
||||
|
||||
def __init__(self, tellstick):
|
||||
self.tellstick = tellstick
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
||||
def __init__(self, tellstick_device):
|
||||
self.tellstick_device = tellstick_device
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick_device.name}
|
||||
self._brightness = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.tellstick.name
|
||||
return self.tellstick_device.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -62,8 +86,9 @@ class TellstickLight(Light):
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
self.tellstick.turn_off()
|
||||
self.tellstick_device.turn_off()
|
||||
self._brightness = 0
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
@@ -74,11 +99,12 @@ class TellstickLight(Light):
|
||||
else:
|
||||
self._brightness = brightness
|
||||
|
||||
self.tellstick.dim(self._brightness)
|
||||
self.tellstick_device.dim(self._brightness)
|
||||
self.update_ha_state()
|
||||
|
||||
def update(self):
|
||||
""" Update state of the light. """
|
||||
last_command = self.tellstick.last_sent_command(
|
||||
last_command = self.tellstick_device.last_sent_command(
|
||||
self.last_sent_command_mask)
|
||||
|
||||
if last_command == tellcore_constants.TELLSTICK_TURNON:
|
||||
@@ -88,6 +114,11 @@ class TellstickLight(Light):
|
||||
elif (last_command == tellcore_constants.TELLSTICK_DIM or
|
||||
last_command == tellcore_constants.TELLSTICK_UP or
|
||||
last_command == tellcore_constants.TELLSTICK_DOWN):
|
||||
last_sent_value = self.tellstick.last_sent_value()
|
||||
last_sent_value = self.tellstick_device.last_sent_value()
|
||||
if last_sent_value is not None:
|
||||
self._brightness = last_sent_value
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant not to poll this entity. """
|
||||
return False
|
||||
|
||||
@@ -1,60 +1,22 @@
|
||||
"""
|
||||
homeassistant.components.light.vera
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Vera lights. This component is useful if you wish for switches
|
||||
connected to your Vera controller to appear as lights in Home Assistant.
|
||||
All switches will be added as a light unless you exclude them in the config.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Vera lights you will need to add something like the following to
|
||||
your configuration.yaml file.
|
||||
|
||||
light:
|
||||
platform: vera
|
||||
vera_controller_url: http://YOUR_VERA_IP:3480/
|
||||
device_data:
|
||||
12:
|
||||
name: My awesome switch
|
||||
exclude: true
|
||||
13:
|
||||
name: Another switch
|
||||
|
||||
Variables:
|
||||
|
||||
vera_controller_url
|
||||
*Required
|
||||
This is the base URL of your vera controller including the port number if not
|
||||
running on 80. Example: http://192.168.1.21:3480/
|
||||
|
||||
device_data
|
||||
*Optional
|
||||
This contains an array additional device info for your Vera devices. It is not
|
||||
required and if not specified all lights configured in your Vera controller
|
||||
will be added with default values. You should use the id of your vera device
|
||||
as the key for the device within device_data.
|
||||
|
||||
These are the variables for the device_data array:
|
||||
|
||||
name
|
||||
*Optional
|
||||
This parameter allows you to override the name of your Vera device in the HA
|
||||
interface, if not specified the value configured for the device in your Vera
|
||||
will be used.
|
||||
|
||||
exclude
|
||||
*Optional
|
||||
This parameter allows you to exclude the specified device from Home Assistant,
|
||||
it should be set to "true" if you want this device excluded.
|
||||
Support for Vera lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.vera/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from homeassistant.components.switch.vera import VeraSwitch
|
||||
|
||||
REQUIREMENTS = ['https://github.com/balloob/home-assistant-vera-api/archive/'
|
||||
'a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip'
|
||||
'#python-vera==0.1']
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
|
||||
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/'
|
||||
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
|
||||
'#python-vera==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,7 +39,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
controller = veraApi.VeraController(base_url)
|
||||
devices = []
|
||||
try:
|
||||
devices = controller.get_devices(['Switch', 'On/Off Switch'])
|
||||
devices = controller.get_devices([
|
||||
'Switch',
|
||||
'On/Off Switch',
|
||||
'Dimmable Switch'])
|
||||
except RequestException:
|
||||
# There was a network related error connecting to the vera controller
|
||||
_LOGGER.exception("Error communicating with Vera API")
|
||||
@@ -89,6 +54,28 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
exclude = extra_data.get('exclude', False)
|
||||
|
||||
if exclude is not True:
|
||||
lights.append(VeraSwitch(device, extra_data))
|
||||
lights.append(VeraLight(device, extra_data))
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
|
||||
class VeraLight(VeraSwitch):
|
||||
""" Represents a Vera Light, including dimmable. """
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes or {}
|
||||
|
||||
if self.vera_device.is_dimmable:
|
||||
attr[ATTR_BRIGHTNESS] = self.vera_device.get_brightness()
|
||||
|
||||
return attr
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable:
|
||||
self.vera_device.set_brightness(kwargs[ATTR_BRIGHTNESS])
|
||||
else:
|
||||
self.vera_device.switch_on()
|
||||
|
||||
self.last_command_send = time.time()
|
||||
self.is_on_status = True
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
homeassistant.components.light.wink
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Wink lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
@@ -10,8 +13,8 @@ from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
|
||||
'c2b700e8ca866159566ecf5e644d9c297f69f257.zip'
|
||||
'#python-wink==0.1']
|
||||
'9eb39eaba0717922815e673ad1114c685839d890.zip'
|
||||
'#python-wink==0.1.1']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
homeassistant.components.light.zwave
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Z-Wave lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.zwave/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
||||
import homeassistant.components.zwave as zwave
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.light import (Light, ATTR_BRIGHTNESS)
|
||||
from threading import Timer
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Find and add Z-Wave lights. """
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||
return
|
||||
if value.type != zwave.TYPE_BYTE:
|
||||
return
|
||||
if value.genre != zwave.GENRE_USER:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveDimmer(value)])
|
||||
|
||||
|
||||
def brightness_state(value):
|
||||
"""
|
||||
Returns the brightness and state according to the current data of given
|
||||
value.
|
||||
"""
|
||||
if value.data > 0:
|
||||
return (value.data / 99) * 255, STATE_ON
|
||||
else:
|
||||
return 255, STATE_OFF
|
||||
|
||||
|
||||
class ZwaveDimmer(Light):
|
||||
""" Provides a Z-Wave dimmer. """
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, value):
|
||||
self._value = value
|
||||
self._node = value.node
|
||||
|
||||
self._brightness, self._state = brightness_state(value)
|
||||
|
||||
# Used for value change event handling
|
||||
self._refreshing = False
|
||||
self._timer = None
|
||||
|
||||
dispatcher.connect(
|
||||
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def _value_changed(self, value):
|
||||
""" Called when a value has changed on the network. """
|
||||
if self._value.value_id != value.value_id:
|
||||
return
|
||||
|
||||
if self._refreshing:
|
||||
self._refreshing = False
|
||||
self._brightness, self._state = brightness_state(value)
|
||||
else:
|
||||
def _refresh_value():
|
||||
"""Used timer callback for delayed value refresh."""
|
||||
self._refreshing = True
|
||||
self._value.refresh()
|
||||
|
||||
if self._timer is not None and self._timer.isAlive():
|
||||
self._timer.cancel()
|
||||
|
||||
self._timer = Timer(2, _refresh_value)
|
||||
self._timer.start()
|
||||
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed for a light. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
name = self._node.name or "{}".format(self._node.product_name)
|
||||
|
||||
return "{}".format(name or self._value.label)
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
""" Brightness of this light between 0..255. """
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self._state == STATE_ON
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
# Zwave multilevel switches use a range of [0, 99] to control
|
||||
# brightness.
|
||||
brightness = (self._brightness / 255) * 99
|
||||
|
||||
if self._node.set_dimmer(self._value.value_id, brightness):
|
||||
self._state = STATE_ON
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
if self._node.set_dimmer(self._value.value_id, 0):
|
||||
self._state = STATE_OFF
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.logbook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Parses events and generates a human log.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/logbook/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
@@ -10,11 +12,12 @@ import re
|
||||
|
||||
from homeassistant.core import State, DOMAIN as HA_DOMAIN
|
||||
from homeassistant.const import (
|
||||
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
|
||||
EVENT_STATE_CHANGED, STATE_NOT_HOME, STATE_ON, STATE_OFF,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
|
||||
from homeassistant import util
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.components.recorder as recorder
|
||||
import homeassistant.components.sun as sun
|
||||
from homeassistant.components import recorder, sun
|
||||
|
||||
|
||||
DOMAIN = "logbook"
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
@@ -25,8 +28,29 @@ QUERY_EVENTS_BETWEEN = """
|
||||
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
|
||||
"""
|
||||
|
||||
EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY'
|
||||
|
||||
GROUP_BY_MINUTES = 15
|
||||
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_MESSAGE = 'message'
|
||||
ATTR_DOMAIN = 'domain'
|
||||
ATTR_ENTITY_ID = 'entity_id'
|
||||
|
||||
|
||||
def log_entry(hass, name, message, domain=None, entity_id=None):
|
||||
""" Adds an entry to the logbook. """
|
||||
data = {
|
||||
ATTR_NAME: name,
|
||||
ATTR_MESSAGE: message
|
||||
}
|
||||
|
||||
if domain is not None:
|
||||
data[ATTR_DOMAIN] = domain
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
hass.bus.fire(EVENT_LOGBOOK_ENTRY, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Listens for download events to download files. """
|
||||
@@ -110,7 +134,10 @@ def humanify(events):
|
||||
# Process events
|
||||
for event in events_batch:
|
||||
if event.event_type == EVENT_STATE_CHANGED:
|
||||
entity_id = event.data['entity_id']
|
||||
entity_id = event.data.get('entity_id')
|
||||
|
||||
if entity_id is None:
|
||||
continue
|
||||
|
||||
if entity_id.startswith('sensor.'):
|
||||
last_sensor_event[entity_id] = event
|
||||
@@ -137,10 +164,12 @@ def humanify(events):
|
||||
|
||||
to_state = State.from_dict(event.data.get('new_state'))
|
||||
|
||||
# if last_changed == last_updated only attributes have changed
|
||||
# we do not report on that yet.
|
||||
# if last_changed != last_updated only attributes have changed
|
||||
# we do not report on that yet. Also filter auto groups.
|
||||
if not to_state or \
|
||||
to_state.last_changed != to_state.last_updated:
|
||||
to_state.last_changed != to_state.last_updated or \
|
||||
to_state.domain == 'group' and \
|
||||
to_state.attributes.get('auto', False):
|
||||
continue
|
||||
|
||||
domain = to_state.domain
|
||||
@@ -175,14 +204,31 @@ def humanify(events):
|
||||
event.time_fired, "Home Assistant", action,
|
||||
domain=HA_DOMAIN)
|
||||
|
||||
elif event.event_type == EVENT_LOGBOOK_ENTRY:
|
||||
domain = event.data.get(ATTR_DOMAIN)
|
||||
entity_id = event.data.get(ATTR_ENTITY_ID)
|
||||
if domain is None and entity_id is not None:
|
||||
try:
|
||||
domain = util.split_entity_id(str(entity_id))[0]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
yield Entry(
|
||||
event.time_fired, event.data.get(ATTR_NAME),
|
||||
event.data.get(ATTR_MESSAGE), domain,
|
||||
entity_id)
|
||||
|
||||
|
||||
def _entry_message_from_state(domain, state):
|
||||
""" Convert a state to a message for the logbook. """
|
||||
# We pass domain in so we don't have to split entity_id again
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
if domain == 'device_tracker':
|
||||
return '{} home'.format(
|
||||
'arrived' if state.state == STATE_HOME else 'left')
|
||||
if state.state == STATE_NOT_HOME:
|
||||
return 'is away'
|
||||
else:
|
||||
return 'is at {}'.format(state.state)
|
||||
|
||||
elif domain == 'sun':
|
||||
if state.state == sun.STATE_ABOVE_HORIZON:
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
homeassistant.components.logger
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component that will help set the level of logging for components.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/logger/
|
||||
"""
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
DOMAIN = 'logger'
|
||||
DEPENDENCIES = []
|
||||
|
||||
LOGSEVERITY = {
|
||||
'CRITICAL': 50,
|
||||
'FATAL': 50,
|
||||
'ERROR': 40,
|
||||
'WARNING': 30,
|
||||
'WARN': 30,
|
||||
'INFO': 20,
|
||||
'DEBUG': 10,
|
||||
'NOTSET': 0
|
||||
}
|
||||
|
||||
LOGGER_DEFAULT = 'default'
|
||||
LOGGER_LOGS = 'logs'
|
||||
|
||||
|
||||
class HomeAssistantLogFilter(logging.Filter):
|
||||
""" A log filter. """
|
||||
# pylint: disable=no-init,too-few-public-methods
|
||||
|
||||
def __init__(self, logfilter):
|
||||
super().__init__()
|
||||
|
||||
self.logfilter = logfilter
|
||||
|
||||
def filter(self, record):
|
||||
|
||||
# Log with filtered severity
|
||||
if LOGGER_LOGS in self.logfilter:
|
||||
for filtername in self.logfilter[LOGGER_LOGS]:
|
||||
logseverity = self.logfilter[LOGGER_LOGS][filtername]
|
||||
if record.name.startswith(filtername):
|
||||
return record.levelno >= logseverity
|
||||
|
||||
# Log with default severity
|
||||
default = self.logfilter[LOGGER_DEFAULT]
|
||||
return record.levelno >= default
|
||||
|
||||
|
||||
def setup(hass, config=None):
|
||||
""" Setup the logger component. """
|
||||
|
||||
logfilter = dict()
|
||||
|
||||
# Set default log severity
|
||||
logfilter[LOGGER_DEFAULT] = LOGSEVERITY['DEBUG']
|
||||
if LOGGER_DEFAULT in config.get(DOMAIN):
|
||||
logfilter[LOGGER_DEFAULT] = LOGSEVERITY[
|
||||
config.get(DOMAIN)[LOGGER_DEFAULT].upper()
|
||||
]
|
||||
|
||||
# Compute log severity for components
|
||||
if LOGGER_LOGS in config.get(DOMAIN):
|
||||
for key, value in config.get(DOMAIN)[LOGGER_LOGS].items():
|
||||
config.get(DOMAIN)[LOGGER_LOGS][key] = LOGSEVERITY[value.upper()]
|
||||
|
||||
logs = OrderedDict(
|
||||
sorted(
|
||||
config.get(DOMAIN)[LOGGER_LOGS].items(),
|
||||
key=lambda t: len(t[0]),
|
||||
reverse=True
|
||||
)
|
||||
)
|
||||
|
||||
logfilter[LOGGER_LOGS] = logs
|
||||
|
||||
# Set log filter for all log handler
|
||||
for handler in logging.root.handlers:
|
||||
handler.addFilter(HomeAssistantLogFilter(logfilter))
|
||||
|
||||
return True
|
||||
@@ -1,12 +1,16 @@
|
||||
"""
|
||||
homeassistant.components.media_player
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Component to interface with various media players.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.const import (
|
||||
@@ -19,15 +23,18 @@ from homeassistant.const import (
|
||||
|
||||
DOMAIN = 'media_player'
|
||||
DEPENDENCIES = []
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = 10
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
discovery.SERVICE_CAST: 'cast',
|
||||
discovery.SERVICE_SONOS: 'sonos',
|
||||
discovery.SERVICE_PLEX: 'plex',
|
||||
}
|
||||
|
||||
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
|
||||
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
|
||||
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
|
||||
@@ -43,6 +50,8 @@ ATTR_MEDIA_TRACK = 'media_track'
|
||||
ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
|
||||
ATTR_MEDIA_SEASON = 'media_season'
|
||||
ATTR_MEDIA_EPISODE = 'media_episode'
|
||||
ATTR_MEDIA_CHANNEL = 'media_channel'
|
||||
ATTR_MEDIA_PLAYLIST = 'media_playlist'
|
||||
ATTR_APP_ID = 'app_id'
|
||||
ATTR_APP_NAME = 'app_name'
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
|
||||
@@ -50,6 +59,9 @@ ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
|
||||
MEDIA_TYPE_MUSIC = 'music'
|
||||
MEDIA_TYPE_TVSHOW = 'tvshow'
|
||||
MEDIA_TYPE_VIDEO = 'movie'
|
||||
MEDIA_TYPE_EPISODE = 'episode'
|
||||
MEDIA_TYPE_CHANNEL = 'channel'
|
||||
MEDIA_TYPE_PLAYLIST = 'playlist'
|
||||
|
||||
SUPPORT_PAUSE = 1
|
||||
SUPPORT_SEEK = 2
|
||||
@@ -60,6 +72,7 @@ SUPPORT_NEXT_TRACK = 32
|
||||
SUPPORT_YOUTUBE = 64
|
||||
SUPPORT_TURN_ON = 128
|
||||
SUPPORT_TURN_OFF = 256
|
||||
SUPPORT_PLAY_MEDIA = 512
|
||||
|
||||
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
|
||||
|
||||
@@ -73,6 +86,7 @@ SERVICE_TO_METHOD = {
|
||||
SERVICE_MEDIA_PAUSE: 'media_pause',
|
||||
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
|
||||
SERVICE_PLAY_MEDIA: 'play_media',
|
||||
}
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
@@ -89,6 +103,8 @@ ATTR_TO_PROPERTY = [
|
||||
ATTR_MEDIA_SERIES_TITLE,
|
||||
ATTR_MEDIA_SEASON,
|
||||
ATTR_MEDIA_EPISODE,
|
||||
ATTR_MEDIA_CHANNEL,
|
||||
ATTR_MEDIA_PLAYLIST,
|
||||
ATTR_APP_ID,
|
||||
ATTR_APP_NAME,
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS,
|
||||
@@ -177,6 +193,16 @@ def media_previous_track(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
|
||||
|
||||
|
||||
def play_media(hass, media_type, media_id, entity_id=None):
|
||||
""" Send the media player the command for playing media. """
|
||||
data = {"media_type": media_type, "media_id": media_id}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for media_players. """
|
||||
component = EntityComponent(
|
||||
@@ -185,6 +211,9 @@ def setup(hass, config):
|
||||
|
||||
component.setup(config)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def media_player_service_handler(service):
|
||||
""" Maps services to methods on MediaPlayerDevice. """
|
||||
target_players = component.extract_from_service(service)
|
||||
@@ -198,7 +227,8 @@ def setup(hass, config):
|
||||
player.update_ha_state(True)
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.register(DOMAIN, service, media_player_service_handler)
|
||||
hass.services.register(DOMAIN, service, media_player_service_handler,
|
||||
descriptions.get(service))
|
||||
|
||||
def volume_set_service(service):
|
||||
""" Set specified volume on the media player. """
|
||||
@@ -215,7 +245,8 @@ def setup(hass, config):
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service,
|
||||
descriptions.get(SERVICE_VOLUME_SET))
|
||||
|
||||
def volume_mute_service(service):
|
||||
""" Mute (true) or unmute (false) the media player. """
|
||||
@@ -232,7 +263,8 @@ def setup(hass, config):
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service)
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service,
|
||||
descriptions.get(SERVICE_VOLUME_MUTE))
|
||||
|
||||
def media_seek_service(service):
|
||||
""" Seek to a position. """
|
||||
@@ -249,7 +281,8 @@ def setup(hass, config):
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service)
|
||||
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service,
|
||||
descriptions.get(SERVICE_MEDIA_SEEK))
|
||||
|
||||
def play_youtube_video_service(service, media_id=None):
|
||||
""" Plays specified media_id on the media player. """
|
||||
@@ -265,16 +298,40 @@ def setup(hass, config):
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
def play_media_service(service):
|
||||
""" Plays specified media_id on the media player. """
|
||||
media_type = service.data.get('media_type')
|
||||
media_id = service.data.get('media_id')
|
||||
|
||||
if media_type is None:
|
||||
return
|
||||
|
||||
if media_id is None:
|
||||
return
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.play_media(media_type, media_id)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, "start_fireplace",
|
||||
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"))
|
||||
lambda service: play_youtube_video_service(service, "eyU3bRy2x44"),
|
||||
descriptions.get('start_fireplace'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, "start_epic_sax",
|
||||
lambda service: play_youtube_video_service(service, "kxopViU98Xo"))
|
||||
lambda service: play_youtube_video_service(service, "kxopViU98Xo"),
|
||||
descriptions.get('start_epic_sax'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service)
|
||||
DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service,
|
||||
descriptions.get(SERVICE_YOUTUBE_VIDEO))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_PLAY_MEDIA, play_media_service,
|
||||
descriptions.get(SERVICE_PLAY_MEDIA))
|
||||
|
||||
return True
|
||||
|
||||
@@ -360,6 +417,16 @@ class MediaPlayerDevice(Entity):
|
||||
""" Episode of current playing media. (TV Show only) """
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_channel(self):
|
||||
""" Channel currently playing. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_playlist(self):
|
||||
""" Title of Playlist currently playing. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
""" ID of the current running app. """
|
||||
@@ -420,6 +487,10 @@ class MediaPlayerDevice(Entity):
|
||||
""" Plays a YouTube media. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
""" Plays a piece of media. """
|
||||
raise NotImplementedError()
|
||||
|
||||
# No need to overwrite these.
|
||||
@property
|
||||
def support_pause(self):
|
||||
@@ -456,6 +527,11 @@ class MediaPlayerDevice(Entity):
|
||||
""" Boolean if YouTube is supported. """
|
||||
return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
|
||||
|
||||
@property
|
||||
def support_play_media(self):
|
||||
""" Boolean if play media command supported. """
|
||||
return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA)
|
||||
|
||||
def volume_up(self):
|
||||
""" volume_up media player. """
|
||||
if self.volume_level < 1:
|
||||
@@ -483,7 +559,7 @@ class MediaPlayerDevice(Entity):
|
||||
else:
|
||||
state_attr = {
|
||||
attr: getattr(self, attr) for attr
|
||||
in ATTR_TO_PROPERTY if getattr(self, attr)
|
||||
in ATTR_TO_PROPERTY if getattr(self, attr) is not None
|
||||
}
|
||||
|
||||
if self.media_image_url:
|
||||
|
||||
@@ -3,22 +3,8 @@ homeassistant.components.media_player.chromecast
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides functionality to interact with Cast devices on the network.
|
||||
|
||||
WARNING: This platform is currently not working due to a changed Cast API.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the chromecast integration you will need to add something like the
|
||||
following to your configuration.yaml file.
|
||||
|
||||
media_player:
|
||||
platform: chromecast
|
||||
host: 192.168.1.9
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Optional
|
||||
Use only if you don't want to scan for devices.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.cast/
|
||||
"""
|
||||
import logging
|
||||
|
||||
@@ -90,6 +76,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class CastDevice(MediaPlayerDevice):
|
||||
""" Represents a Cast device on the network. """
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, host):
|
||||
|
||||
@@ -2,42 +2,9 @@
|
||||
homeassistant.components.media_player.denon
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides an interface to Denon Network Receivers.
|
||||
Developed for a Denon DRA-N5, see
|
||||
http://www.denon.co.uk/chg/product/compactsystems/networkmusicsystems/ceolpiccolo
|
||||
|
||||
A few notes:
|
||||
- As long as this module is active and connected, the receiver does
|
||||
not seem to accept additional telnet connections.
|
||||
|
||||
- Be careful with the volume. 50% or even 100% are very loud.
|
||||
|
||||
- To be able to wake up the receiver, activate the "remote" setting
|
||||
in the receiver's settings.
|
||||
|
||||
- Play and pause are supported, toggling is not possible.
|
||||
|
||||
- Seeking cannot be implemented as the UI sends absolute positions.
|
||||
Only seeking via simulated button presses is possible.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use your Denon you will need to add something like the following to
|
||||
your config/configuration.yaml:
|
||||
|
||||
media_player:
|
||||
platform: denon
|
||||
name: Music station
|
||||
host: 192.168.0.123
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
The ip of the player. Example: 192.168.0.123
|
||||
|
||||
name
|
||||
*Optional
|
||||
The name of the device.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.denon/
|
||||
"""
|
||||
import telnetlib
|
||||
import logging
|
||||
@@ -67,13 +34,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
CONF_HOST)
|
||||
return False
|
||||
|
||||
add_devices([
|
||||
DenonDevice(
|
||||
config.get('name', 'Music station'),
|
||||
config.get('host'))
|
||||
])
|
||||
|
||||
return True
|
||||
denon = DenonDevice(
|
||||
config.get("name", "Music station"),
|
||||
config.get("host")
|
||||
)
|
||||
if denon.update():
|
||||
add_devices([denon])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class DenonDevice(MediaPlayerDevice):
|
||||
@@ -84,28 +53,41 @@ class DenonDevice(MediaPlayerDevice):
|
||||
def __init__(self, name, host):
|
||||
self._name = name
|
||||
self._host = host
|
||||
self._telnet = telnetlib.Telnet(self._host)
|
||||
self._pwstate = "PWSTANDBY"
|
||||
self._volume = 0
|
||||
self._muted = False
|
||||
self._mediasource = ""
|
||||
|
||||
def query(self, message):
|
||||
""" Send request and await response from server """
|
||||
@classmethod
|
||||
def telnet_request(cls, telnet, command):
|
||||
""" Executes `command` and returns the response. """
|
||||
telnet.write(command.encode("ASCII") + b"\r")
|
||||
return telnet.read_until(b"\r", timeout=0.2).decode("ASCII").strip()
|
||||
|
||||
def telnet_command(self, command):
|
||||
""" Establishes a telnet connection and sends `command`. """
|
||||
telnet = telnetlib.Telnet(self._host)
|
||||
telnet.write(command.encode("ASCII") + b"\r")
|
||||
telnet.read_very_eager() # skip response
|
||||
telnet.close()
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
# unspecified command, should be ignored
|
||||
self._telnet.write("?".encode('UTF-8') + b'\r')
|
||||
except (EOFError, BrokenPipeError, ConnectionResetError):
|
||||
self._telnet.open(self._host)
|
||||
telnet = telnetlib.Telnet(self._host)
|
||||
except ConnectionRefusedError:
|
||||
return False
|
||||
|
||||
self._telnet.read_very_eager() # skip what is not requested
|
||||
self._pwstate = self.telnet_request(telnet, "PW?")
|
||||
# PW? sends also SISTATUS, which is not interesting
|
||||
telnet.read_until(b"\r", timeout=0.2)
|
||||
|
||||
self._telnet.write(message.encode('ASCII') + b'\r')
|
||||
# timeout 200ms, defined by protocol
|
||||
resp = self._telnet.read_until(b'\r', timeout=0.2)\
|
||||
.decode('UTF-8').strip()
|
||||
volume_str = self.telnet_request(telnet, "MV?")[len("MV"):]
|
||||
self._volume = int(volume_str) / 60
|
||||
self._muted = (self.telnet_request(telnet, "MU?") == "MUON")
|
||||
self._mediasource = self.telnet_request(telnet, "SI?")[len("SI"):]
|
||||
|
||||
if message == "PW?":
|
||||
# workaround; PW? sends also SISTATUS
|
||||
self._telnet.read_until(b'\r', timeout=0.2)
|
||||
|
||||
return resp
|
||||
telnet.close()
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -115,10 +97,9 @@ class DenonDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
pwstate = self.query('PW?')
|
||||
if pwstate == "PWSTANDBY":
|
||||
if self._pwstate == "PWSTANDBY":
|
||||
return STATE_OFF
|
||||
if pwstate == "PWON":
|
||||
if self._pwstate == "PWON":
|
||||
return STATE_ON
|
||||
|
||||
return STATE_UNKNOWN
|
||||
@@ -126,17 +107,17 @@ class DenonDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def volume_level(self):
|
||||
""" Volume level of the media player (0..1). """
|
||||
return int(self.query('MV?')[len('MV'):]) / 60
|
||||
return self._volume
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
""" Boolean if volume is currently muted. """
|
||||
return self.query('MU?') == "MUON"
|
||||
return self._muted
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Current media source. """
|
||||
return self.query('SI?')[len('SI'):]
|
||||
return self._mediasource
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
@@ -145,24 +126,24 @@ class DenonDevice(MediaPlayerDevice):
|
||||
|
||||
def turn_off(self):
|
||||
""" turn_off media player. """
|
||||
self.query('PWSTANDBY')
|
||||
self.telnet_command("PWSTANDBY")
|
||||
|
||||
def volume_up(self):
|
||||
""" volume_up media player. """
|
||||
self.query('MVUP')
|
||||
self.telnet_command("MVUP")
|
||||
|
||||
def volume_down(self):
|
||||
""" volume_down media player. """
|
||||
self.query('MVDOWN')
|
||||
self.telnet_command("MVDOWN")
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
""" set volume level, range 0..1. """
|
||||
# 60dB max
|
||||
self.query('MV' + str(round(volume * 60)).zfill(2))
|
||||
self.telnet_command("MV" + str(round(volume * 60)).zfill(2))
|
||||
|
||||
def mute_volume(self, mute):
|
||||
""" mute (true) or unmute (false) media player. """
|
||||
self.query('MU' + ('ON' if mute else 'OFF'))
|
||||
self.telnet_command("MU" + ("ON" if mute else "OFF"))
|
||||
|
||||
def media_play_pause(self):
|
||||
""" media_play_pause media player. """
|
||||
@@ -170,22 +151,22 @@ class DenonDevice(MediaPlayerDevice):
|
||||
|
||||
def media_play(self):
|
||||
""" media_play media player. """
|
||||
self.query('NS9A')
|
||||
self.telnet_command("NS9A")
|
||||
|
||||
def media_pause(self):
|
||||
""" media_pause media player. """
|
||||
self.query('NS9B')
|
||||
self.telnet_command("NS9B")
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command. """
|
||||
self.query('NS9D')
|
||||
self.telnet_command("NS9D")
|
||||
|
||||
def media_previous_track(self):
|
||||
self.query('NS9E')
|
||||
self.telnet_command("NS9E")
|
||||
|
||||
def media_seek(self, position):
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_on(self):
|
||||
""" turn the media player on. """
|
||||
self.query('PWON')
|
||||
self.telnet_command("PWON")
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
homeassistant.components.media_player.firetv
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides functionality to interact with FireTV devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.firetv/
|
||||
"""
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF,
|
||||
STATE_UNKNOWN, STATE_STANDBY)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice,
|
||||
SUPPORT_PAUSE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK)
|
||||
|
||||
SUPPORT_FIRETV = SUPPORT_PAUSE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET
|
||||
|
||||
DOMAIN = 'firetv'
|
||||
DEVICE_LIST_URL = 'http://{0}/devices/list'
|
||||
DEVICE_STATE_URL = 'http://{0}/devices/state/{1}'
|
||||
DEVICE_ACTION_URL = 'http://{0}/devices/action/{1}/{2}'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the FireTV platform. """
|
||||
host = config.get('host', 'localhost:5556')
|
||||
device_id = config.get('device', 'default')
|
||||
try:
|
||||
response = requests.get(DEVICE_LIST_URL.format(host)).json()
|
||||
if device_id in response['devices'].keys():
|
||||
add_devices([
|
||||
FireTVDevice(
|
||||
host,
|
||||
device_id,
|
||||
config.get('name', 'Amazon Fire TV')
|
||||
)
|
||||
])
|
||||
_LOGGER.info(
|
||||
'Device %s accessible and ready for control', device_id)
|
||||
else:
|
||||
_LOGGER.warn(
|
||||
'Device %s is not registered with firetv-server', device_id)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error('Could not connect to firetv-server at %s', host)
|
||||
|
||||
|
||||
class FireTV(object):
|
||||
""" firetv-server client.
|
||||
|
||||
Should a native Python 3 ADB module become available, python-firetv can
|
||||
support Python 3, it can be added as a dependency, and this class can be
|
||||
dispensed of.
|
||||
|
||||
For now, it acts as a client to the firetv-server HTTP server (which must
|
||||
be running via Python 2).
|
||||
"""
|
||||
|
||||
def __init__(self, host, device_id):
|
||||
self.host = host
|
||||
self.device_id = device_id
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Get the device state. An exception means UNKNOWN state. """
|
||||
try:
|
||||
response = requests.get(
|
||||
DEVICE_STATE_URL.format(
|
||||
self.host,
|
||||
self.device_id
|
||||
)
|
||||
).json()
|
||||
return response.get('state', STATE_UNKNOWN)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error(
|
||||
'Could not retrieve device state for %s', self.device_id)
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def action(self, action_id):
|
||||
""" Perform an action on the device. """
|
||||
try:
|
||||
requests.get(
|
||||
DEVICE_ACTION_URL.format(
|
||||
self.host,
|
||||
self.device_id,
|
||||
action_id
|
||||
)
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error(
|
||||
'Action request for %s was not accepted for device %s',
|
||||
action_id, self.device_id)
|
||||
|
||||
|
||||
class FireTVDevice(MediaPlayerDevice):
|
||||
""" Represents an Amazon Fire TV device on the network. """
|
||||
|
||||
def __init__(self, host, device, name):
|
||||
self._firetv = FireTV(host, device)
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Get the device name. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Device should be polled. """
|
||||
return True
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_FIRETV
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" State of the player. """
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
""" Update device state. """
|
||||
self._state = {
|
||||
'idle': STATE_IDLE,
|
||||
'off': STATE_OFF,
|
||||
'play': STATE_PLAYING,
|
||||
'pause': STATE_PAUSED,
|
||||
'standby': STATE_STANDBY,
|
||||
'disconnected': STATE_UNKNOWN,
|
||||
}.get(self._firetv.state, STATE_UNKNOWN)
|
||||
|
||||
def turn_on(self):
|
||||
""" Turns on the device. """
|
||||
self._firetv.action('turn_on')
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns off the device. """
|
||||
self._firetv.action('turn_off')
|
||||
|
||||
def media_play(self):
|
||||
""" Send play command. """
|
||||
self._firetv.action('media_play')
|
||||
|
||||
def media_pause(self):
|
||||
""" Send pause command. """
|
||||
self._firetv.action('media_pause')
|
||||
|
||||
def media_play_pause(self):
|
||||
""" Send play/pause command. """
|
||||
self._firetv.action('media_play_pause')
|
||||
|
||||
def volume_up(self):
|
||||
""" Send volume up command. """
|
||||
self._firetv.action('volume_up')
|
||||
|
||||
def volume_down(self):
|
||||
""" Send volume down command. """
|
||||
self._firetv.action('volume_down')
|
||||
|
||||
def media_previous_track(self):
|
||||
""" Send previous track command (results in rewind). """
|
||||
self._firetv.action('media_previous')
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command (results in fast-forward). """
|
||||
self._firetv.action('media_next')
|
||||
|
||||
def media_seek(self, position):
|
||||
raise NotImplementedError()
|
||||
|
||||
def mute_volume(self, mute):
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_youtube(self, media_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,443 @@
|
||||
"""
|
||||
homeassistant.components.media_player.itunes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides an interface to iTunes API.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.itunes/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_PAUSE,
|
||||
SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON,
|
||||
SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA,
|
||||
ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON)
|
||||
|
||||
import requests
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
SUPPORT_PLAY_MEDIA
|
||||
|
||||
SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
|
||||
DOMAIN = 'itunes'
|
||||
|
||||
|
||||
class Itunes(object):
|
||||
""" itunes-api client. """
|
||||
|
||||
def __init__(self, host, port):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
@property
|
||||
def _base_url(self):
|
||||
""" Returns the base url for endpoints. """
|
||||
return self.host + ":" + str(self.port)
|
||||
|
||||
def _request(self, method, path, params=None):
|
||||
""" Makes the actual request and returns the parsed response. """
|
||||
url = self._base_url + path
|
||||
|
||||
try:
|
||||
if method == 'GET':
|
||||
response = requests.get(url)
|
||||
elif method == "POST":
|
||||
response = requests.put(url, params)
|
||||
elif method == "PUT":
|
||||
response = requests.put(url, params)
|
||||
elif method == "DELETE":
|
||||
response = requests.delete(url)
|
||||
|
||||
return response.json()
|
||||
except requests.exceptions.HTTPError:
|
||||
return {'player_state': 'error'}
|
||||
except requests.exceptions.RequestException:
|
||||
return {'player_state': 'offline'}
|
||||
|
||||
def _command(self, named_command):
|
||||
""" Makes a request for a controlling command. """
|
||||
return self._request('PUT', '/' + named_command)
|
||||
|
||||
def now_playing(self):
|
||||
""" Returns the current state. """
|
||||
return self._request('GET', '/now_playing')
|
||||
|
||||
def set_volume(self, level):
|
||||
""" Sets the volume and returns the current state, level 0-100. """
|
||||
return self._request('PUT', '/volume', {'level': level})
|
||||
|
||||
def set_muted(self, muted):
|
||||
""" Mutes and returns the current state, muted True or False. """
|
||||
return self._request('PUT', '/mute', {'muted': muted})
|
||||
|
||||
def play(self):
|
||||
""" Sets playback to play and returns the current state. """
|
||||
return self._command('play')
|
||||
|
||||
def pause(self):
|
||||
""" Sets playback to paused and returns the current state. """
|
||||
return self._command('pause')
|
||||
|
||||
def next(self):
|
||||
""" Skips to the next track and returns the current state. """
|
||||
return self._command('next')
|
||||
|
||||
def previous(self):
|
||||
""" Skips back and returns the current state. """
|
||||
return self._command('previous')
|
||||
|
||||
def play_playlist(self, playlist_id_or_name):
|
||||
""" Sets a playlist to be current and returns the current state. """
|
||||
response = self._request('GET', '/playlists')
|
||||
playlists = response.get('playlists', [])
|
||||
|
||||
found_playlists = \
|
||||
[playlist for playlist in playlists if
|
||||
(playlist_id_or_name in [playlist["name"], playlist["id"]])]
|
||||
|
||||
if len(found_playlists) > 0:
|
||||
playlist = found_playlists[0]
|
||||
path = '/playlists/' + playlist['id'] + '/play'
|
||||
return self._request('PUT', path)
|
||||
|
||||
def artwork_url(self):
|
||||
""" Returns a URL of the current track's album art. """
|
||||
return self._base_url + '/artwork'
|
||||
|
||||
def airplay_devices(self):
|
||||
""" Returns a list of AirPlay devices. """
|
||||
return self._request('GET', '/airplay_devices')
|
||||
|
||||
def airplay_device(self, device_id):
|
||||
""" Returns an AirPlay device. """
|
||||
return self._request('GET', '/airplay_devices/' + device_id)
|
||||
|
||||
def toggle_airplay_device(self, device_id, toggle):
|
||||
""" Toggles airplay device on or off, id, toggle True or False. """
|
||||
command = 'on' if toggle else 'off'
|
||||
path = '/airplay_devices/' + device_id + '/' + command
|
||||
return self._request('PUT', path)
|
||||
|
||||
def set_volume_airplay_device(self, device_id, level):
|
||||
""" Sets volume, returns current state of device, id,level 0-100. """
|
||||
path = '/airplay_devices/' + device_id + '/volume'
|
||||
return self._request('PUT', path, {'level': level})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, abstract-method
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the itunes platform. """
|
||||
|
||||
add_devices([
|
||||
ItunesDevice(
|
||||
config.get('name', 'iTunes'),
|
||||
config.get('host'),
|
||||
config.get('port'),
|
||||
add_devices
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
class ItunesDevice(MediaPlayerDevice):
|
||||
""" Represents a iTunes-API instance. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
def __init__(self, name, host, port, add_devices):
|
||||
self._name = name
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._add_devices = add_devices
|
||||
|
||||
self.client = Itunes(self._host, self._port)
|
||||
|
||||
self.current_volume = None
|
||||
self.muted = None
|
||||
self.current_title = None
|
||||
self.current_album = None
|
||||
self.current_artist = None
|
||||
self.current_playlist = None
|
||||
self.content_id = None
|
||||
|
||||
self.player_state = None
|
||||
|
||||
self.airplay_devices = {}
|
||||
|
||||
self.update()
|
||||
|
||||
def update_state(self, state_hash):
|
||||
""" Update all the state properties with the passed in dictionary. """
|
||||
self.player_state = state_hash.get('player_state', None)
|
||||
|
||||
self.current_volume = state_hash.get('volume', 0)
|
||||
self.muted = state_hash.get('muted', None)
|
||||
self.current_title = state_hash.get('name', None)
|
||||
self.current_album = state_hash.get('album', None)
|
||||
self.current_artist = state_hash.get('artist', None)
|
||||
self.current_playlist = state_hash.get('playlist', None)
|
||||
self.content_id = state_hash.get('id', None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
|
||||
if self.player_state == 'offline' or self.player_state is None:
|
||||
return 'offline'
|
||||
|
||||
if self.player_state == 'error':
|
||||
return 'error'
|
||||
|
||||
if self.player_state == 'stopped':
|
||||
return STATE_IDLE
|
||||
|
||||
if self.player_state == 'paused':
|
||||
return STATE_PAUSED
|
||||
else:
|
||||
return STATE_PLAYING
|
||||
|
||||
def update(self):
|
||||
""" Retrieve latest state. """
|
||||
now_playing = self.client.now_playing()
|
||||
self.update_state(now_playing)
|
||||
|
||||
found_devices = self.client.airplay_devices()
|
||||
found_devices = found_devices.get('airplay_devices', [])
|
||||
|
||||
new_devices = []
|
||||
|
||||
for device_data in found_devices:
|
||||
device_id = device_data.get('id')
|
||||
|
||||
if self.airplay_devices.get(device_id):
|
||||
# update it
|
||||
airplay_device = self.airplay_devices.get(device_id)
|
||||
airplay_device.update_state(device_data)
|
||||
else:
|
||||
# add it
|
||||
airplay_device = AirPlayDevice(device_id, self.client)
|
||||
airplay_device.update_state(device_data)
|
||||
self.airplay_devices[device_id] = airplay_device
|
||||
new_devices.append(airplay_device)
|
||||
|
||||
if new_devices:
|
||||
self._add_devices(new_devices)
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
""" Boolean if volume is currently muted. """
|
||||
return self.muted
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
""" Volume level of the media player (0..1). """
|
||||
return self.current_volume/100.0
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
return self.content_id
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
""" Content type of current playing media. """
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
|
||||
if self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) and \
|
||||
self.current_title is not None:
|
||||
return self.client.artwork_url()
|
||||
else:
|
||||
return 'https://cloud.githubusercontent.com/assets/260/9829355' \
|
||||
'/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png'
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
return self.current_title
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
""" Artist of current playing media. (Music track only) """
|
||||
return self.current_artist
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
""" Album of current playing media. (Music track only) """
|
||||
return self.current_album
|
||||
|
||||
@property
|
||||
def media_playlist(self):
|
||||
""" Title of the currently playing playlist. """
|
||||
return self.current_playlist
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_ITUNES
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
""" set volume level, range 0..1. """
|
||||
response = self.client.set_volume(int(volume * 100))
|
||||
self.update_state(response)
|
||||
|
||||
def mute_volume(self, mute):
|
||||
""" mute (true) or unmute (false) media player. """
|
||||
response = self.client.set_muted(mute)
|
||||
self.update_state(response)
|
||||
|
||||
def media_play(self):
|
||||
""" media_play media player. """
|
||||
response = self.client.play()
|
||||
self.update_state(response)
|
||||
|
||||
def media_pause(self):
|
||||
""" media_pause media player. """
|
||||
response = self.client.pause()
|
||||
self.update_state(response)
|
||||
|
||||
def media_next_track(self):
|
||||
""" media_next media player. """
|
||||
response = self.client.next()
|
||||
self.update_state(response)
|
||||
|
||||
def media_previous_track(self):
|
||||
""" media_previous media player. """
|
||||
response = self.client.previous()
|
||||
self.update_state(response)
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
""" play_media media player. """
|
||||
if media_type == MEDIA_TYPE_PLAYLIST:
|
||||
response = self.client.play_playlist(media_id)
|
||||
self.update_state(response)
|
||||
|
||||
|
||||
class AirPlayDevice(MediaPlayerDevice):
|
||||
""" Represents an AirPlay device via an iTunes-API instance. """
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, device_id, client):
|
||||
self._id = device_id
|
||||
|
||||
self.client = client
|
||||
|
||||
self.device_name = "AirPlay"
|
||||
self.kind = None
|
||||
self.active = False
|
||||
self.selected = False
|
||||
self.volume = 0
|
||||
self.supports_audio = False
|
||||
self.supports_video = False
|
||||
self.player_state = None
|
||||
|
||||
def update_state(self, state_hash):
|
||||
""" Update all the state properties with the passed in dictionary. """
|
||||
|
||||
if 'player_state' in state_hash:
|
||||
self.player_state = state_hash.get('player_state', None)
|
||||
|
||||
if 'name' in state_hash:
|
||||
name = state_hash.get('name', '')
|
||||
self.device_name = (name + ' AirTunes Speaker').strip()
|
||||
|
||||
if 'kind' in state_hash:
|
||||
self.kind = state_hash.get('kind', None)
|
||||
|
||||
if 'active' in state_hash:
|
||||
self.active = state_hash.get('active', None)
|
||||
|
||||
if 'selected' in state_hash:
|
||||
self.selected = state_hash.get('selected', None)
|
||||
|
||||
if 'sound_volume' in state_hash:
|
||||
self.volume = state_hash.get('sound_volume', 0)
|
||||
|
||||
if 'supports_audio' in state_hash:
|
||||
self.supports_audio = state_hash.get('supports_audio', None)
|
||||
|
||||
if 'supports_video' in state_hash:
|
||||
self.supports_video = state_hash.get('supports_video', None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self.device_name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
|
||||
if self.selected is True:
|
||||
return STATE_ON
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
def update(self):
|
||||
""" Retrieve latest state. """
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
return float(self.volume)/100.0
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_AIRPLAY
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Return the state attributes. """
|
||||
state_attr = {}
|
||||
state_attr[ATTR_SUPPORTED_MEDIA_COMMANDS] = SUPPORT_AIRPLAY
|
||||
|
||||
if self.state == STATE_OFF:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = \
|
||||
('https://cloud.githubusercontent.com/assets/260/9833073'
|
||||
'/6eb5c906-5958-11e5-9b4a-472cdf36be16.png')
|
||||
else:
|
||||
state_attr[ATTR_ENTITY_PICTURE] = \
|
||||
('https://cloud.githubusercontent.com/assets/260/9833072'
|
||||
'/6eb13cce-5958-11e5-996f-e2aaefbc9a24.png')
|
||||
|
||||
return state_attr
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
""" set volume level, range 0..1. """
|
||||
volume = int(volume * 100)
|
||||
response = self.client.set_volume_airplay_device(self._id, volume)
|
||||
self.update_state(response)
|
||||
|
||||
def turn_on(self):
|
||||
""" Select AirPlay. """
|
||||
self.update_state({"selected": True})
|
||||
self.update_ha_state()
|
||||
response = self.client.toggle_airplay_device(self._id, True)
|
||||
self.update_state(response)
|
||||
|
||||
def turn_off(self):
|
||||
""" Deselect AirPlay. """
|
||||
self.update_state({"selected": False})
|
||||
self.update_ha_state()
|
||||
response = self.client.toggle_airplay_device(self._id, False)
|
||||
self.update_state(response)
|
||||
@@ -3,35 +3,8 @@ homeassistant.components.media_player.kodi
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides an interface to the XBMC/Kodi JSON-RPC API
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Kodi you will need to add something like the following to
|
||||
your configuration.yaml file.
|
||||
|
||||
media_player:
|
||||
platform: kodi
|
||||
name: Kodi
|
||||
url: http://192.168.0.123/jsonrpc
|
||||
user: kodi
|
||||
password: my_secure_password
|
||||
|
||||
Variables:
|
||||
|
||||
name
|
||||
*Optional
|
||||
The name of the device.
|
||||
|
||||
url
|
||||
*Required
|
||||
The URL of the XBMC/Kodi JSON-RPC API. Example: http://192.168.0.123/jsonrpc
|
||||
|
||||
user
|
||||
*Optional
|
||||
The XBMC/Kodi HTTP username.
|
||||
|
||||
password
|
||||
*Optional
|
||||
The XBMC/Kodi HTTP password.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.kodi/
|
||||
"""
|
||||
import urllib
|
||||
import logging
|
||||
@@ -107,6 +80,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
try:
|
||||
return self._server.Player.GetActivePlayers()
|
||||
except jsonrpc_requests.jsonrpc.TransportError:
|
||||
_LOGGER.exception('Unable to fetch kodi data')
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -166,7 +140,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
if self._item is not None:
|
||||
return self._item['uniqueid']
|
||||
return self._item.get('uniqueid', None)
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
|
||||
@@ -3,35 +3,8 @@ homeassistant.components.media_player.mpd
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides functionality to interact with a Music Player Daemon.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use MPD you will need to add something like the following to your
|
||||
configuration.yaml file.
|
||||
|
||||
media_player:
|
||||
platform: mpd
|
||||
server: 127.0.0.1
|
||||
port: 6600
|
||||
location: bedroom
|
||||
password: superSecretPassword123
|
||||
|
||||
Variables:
|
||||
|
||||
server
|
||||
*Required
|
||||
IP address of the Music Player Daemon. Example: 192.168.1.32
|
||||
|
||||
port
|
||||
*Optional
|
||||
Port of the Music Player Daemon, defaults to 6600. Example: 6600
|
||||
|
||||
location
|
||||
*Optional
|
||||
Location of your Music Player Daemon.
|
||||
|
||||
password
|
||||
*Optional
|
||||
Password for your Music Player Daemon.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.mpd/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
"""
|
||||
homeassistant.components.media_player.plex
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Provides an interface to the Plex API.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.plex/
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
||||
from homeassistant.const import (
|
||||
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_PLAYING,
|
||||
STATE_PAUSED, STATE_OFF, STATE_UNKNOWN)
|
||||
|
||||
REQUIREMENTS = ['plexapi==1.1.0']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
|
||||
PLEX_CONFIG_FILE = 'plex.conf'
|
||||
|
||||
# Map ip to request id for configuring
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
|
||||
|
||||
def config_from_file(filename, config=None):
|
||||
''' Small configuration file management function'''
|
||||
if config:
|
||||
# We're writing configuration
|
||||
try:
|
||||
with open(filename, 'w') as fdesc:
|
||||
fdesc.write(json.dumps(config))
|
||||
except IOError as error:
|
||||
_LOGGER.error('Saving config file failed: %s', error)
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
# We're reading config
|
||||
if os.path.isfile(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fdesc:
|
||||
return json.loads(fdesc.read())
|
||||
except IOError as error:
|
||||
_LOGGER.error('Reading config file failed: %s', error)
|
||||
# This won't work yet
|
||||
return False
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
# pylint: disable=abstract-method, unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Sets up the plex platform. """
|
||||
|
||||
config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
|
||||
if len(config):
|
||||
# Setup a configured PlexServer
|
||||
host, token = config.popitem()
|
||||
token = token['token']
|
||||
# Via discovery
|
||||
elif discovery_info is not None:
|
||||
# Parse discovery data
|
||||
host = urlparse(discovery_info[1]).netloc
|
||||
_LOGGER.info('Discovered PLEX server: %s', host)
|
||||
|
||||
if host in _CONFIGURING:
|
||||
return
|
||||
token = None
|
||||
else:
|
||||
return
|
||||
|
||||
setup_plexserver(host, token, hass, add_devices_callback)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup_plexserver(host, token, hass, add_devices_callback):
|
||||
''' Setup a plexserver based on host parameter'''
|
||||
import plexapi.server
|
||||
import plexapi.exceptions
|
||||
|
||||
try:
|
||||
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
|
||||
except (plexapi.exceptions.BadRequest,
|
||||
plexapi.exceptions.Unauthorized,
|
||||
plexapi.exceptions.NotFound) as error:
|
||||
_LOGGER.info(error)
|
||||
# No token or wrong token
|
||||
request_configuration(host, hass, add_devices_callback)
|
||||
return
|
||||
|
||||
# If we came here and configuring this host, mark as done
|
||||
if host in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(host)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
_LOGGER.info('Discovery configuration done!')
|
||||
|
||||
# Save config
|
||||
if not config_from_file(
|
||||
hass.config.path(PLEX_CONFIG_FILE),
|
||||
{host: {'token': token}}):
|
||||
_LOGGER.error('failed to save config file')
|
||||
|
||||
_LOGGER.info('Connected to: htts://%s', host)
|
||||
|
||||
plex_clients = {}
|
||||
plex_sessions = {}
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_devices():
|
||||
""" Updates the devices objects. """
|
||||
try:
|
||||
devices = plexserver.clients()
|
||||
except plexapi.exceptions.BadRequest:
|
||||
_LOGGER.exception("Error listing plex devices")
|
||||
return
|
||||
|
||||
new_plex_clients = []
|
||||
for device in devices:
|
||||
# For now, let's allow all deviceClass types
|
||||
if device.deviceClass in ['badClient']:
|
||||
continue
|
||||
|
||||
if device.machineIdentifier not in plex_clients:
|
||||
new_client = PlexClient(device, plex_sessions, update_devices,
|
||||
update_sessions)
|
||||
plex_clients[device.machineIdentifier] = new_client
|
||||
new_plex_clients.append(new_client)
|
||||
else:
|
||||
plex_clients[device.machineIdentifier].set_device(device)
|
||||
|
||||
if new_plex_clients:
|
||||
add_devices_callback(new_plex_clients)
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_sessions():
|
||||
""" Updates the sessions objects. """
|
||||
try:
|
||||
sessions = plexserver.sessions()
|
||||
except plexapi.exceptions.BadRequest:
|
||||
_LOGGER.exception("Error listing plex sessions")
|
||||
return
|
||||
|
||||
plex_sessions.clear()
|
||||
for session in sessions:
|
||||
plex_sessions[session.player.machineIdentifier] = session
|
||||
|
||||
update_devices()
|
||||
update_sessions()
|
||||
|
||||
|
||||
def request_configuration(host, hass, add_devices_callback):
|
||||
""" Request configuration steps from the user. """
|
||||
configurator = get_component('configurator')
|
||||
|
||||
# We got an error if this method is called while we are configuring
|
||||
if host in _CONFIGURING:
|
||||
configurator.notify_errors(
|
||||
_CONFIGURING[host], "Failed to register, please try again.")
|
||||
|
||||
return
|
||||
|
||||
def plex_configuration_callback(data):
|
||||
""" Actions to do when our configuration callback is called. """
|
||||
setup_plexserver(host, data.get('token'), hass, add_devices_callback)
|
||||
|
||||
_CONFIGURING[host] = configurator.request_config(
|
||||
hass, "Plex Media Server", plex_configuration_callback,
|
||||
description=('Enter the X-Plex-Token'),
|
||||
description_image="/static/images/config_plex_mediaserver.png",
|
||||
submit_caption="Confirm",
|
||||
fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}]
|
||||
)
|
||||
|
||||
|
||||
class PlexClient(MediaPlayerDevice):
|
||||
""" Represents a Plex device. """
|
||||
|
||||
# pylint: disable=too-many-public-methods, attribute-defined-outside-init
|
||||
def __init__(self, device, plex_sessions, update_devices, update_sessions):
|
||||
self.plex_sessions = plex_sessions
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
self.set_device(device)
|
||||
|
||||
def set_device(self, device):
|
||||
""" Sets the device property. """
|
||||
self.device = device
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this plex client """
|
||||
return "{}.{}".format(
|
||||
self.__class__, self.device.machineIdentifier or self.device.name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self.device.name or DEVICE_DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
""" Returns the session, if any. """
|
||||
if self.device.machineIdentifier not in self.plex_sessions:
|
||||
return None
|
||||
|
||||
return self.plex_sessions[self.device.machineIdentifier]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self.session:
|
||||
state = self.session.player.state
|
||||
if state == 'playing':
|
||||
return STATE_PLAYING
|
||||
elif state == 'paused':
|
||||
return STATE_PAUSED
|
||||
# This is nasty. Need to find a way to determine alive
|
||||
elif self.device:
|
||||
return STATE_IDLE
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def update(self):
|
||||
self.update_devices(no_throttle=True)
|
||||
self.update_sessions(no_throttle=True)
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
if self.session is not None:
|
||||
return self.session.ratingKey
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
""" Content type of current playing media. """
|
||||
if self.session is None:
|
||||
return None
|
||||
media_type = self.session.type
|
||||
if media_type == 'episode':
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
elif media_type == 'movie':
|
||||
return MEDIA_TYPE_VIDEO
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
""" Duration of current playing media in seconds. """
|
||||
if self.session is not None:
|
||||
return self.session.duration
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
if self.session is not None:
|
||||
return self.session.thumbUrl
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
# find a string we can use as a title
|
||||
if self.session is not None:
|
||||
return self.session.title
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
""" Season of curent playing media (TV Show only). """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.seasons()[0].index
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
""" Series title of current playing media (TV Show only). """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.grandparentTitle
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
""" Episode of current playing media (TV Show only). """
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.index
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
return SUPPORT_PLEX
|
||||
|
||||
def media_play(self):
|
||||
""" media_play media player. """
|
||||
self.device.play()
|
||||
|
||||
def media_pause(self):
|
||||
""" media_pause media player. """
|
||||
self.device.pause()
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command. """
|
||||
self.device.skipNext()
|
||||
|
||||
def media_previous_track(self):
|
||||
""" Send previous track command. """
|
||||
self.device.skipPrevious()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user