Compare commits
654 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aaf49d0c1 | ||
|
|
6f84fa4ce5 | ||
|
|
a1c0544e35 | ||
|
|
a59d26b1fa | ||
|
|
170a0c9888 | ||
|
|
e084a260c6 | ||
|
|
8709a397f6 | ||
|
|
9fed3acc90 | ||
|
|
0713dbc7a3 | ||
|
|
5bdbf3dfc0 | ||
|
|
5bde72d490 | ||
|
|
af69a307a8 | ||
|
|
7bbef68b2a | ||
|
|
abde8c40c9 | ||
|
|
07b2f38046 | ||
|
|
280c1601a2 | ||
|
|
df24ecf395 | ||
|
|
3d8e425113 | ||
|
|
a8ee11e732 | ||
|
|
748fff7ebc | ||
|
|
9a71717047 | ||
|
|
920f9f132b | ||
|
|
3aa3130d05 | ||
|
|
caa16da5c5 | ||
|
|
94e270f828 | ||
|
|
502ebd2a31 | ||
|
|
0db9c04f21 | ||
|
|
72bb0e97e0 | ||
|
|
5123487705 | ||
|
|
3d9ff372fc | ||
|
|
9123dfce6d | ||
|
|
27edbe5c0f | ||
|
|
ffe832763d | ||
|
|
becd94fe2f | ||
|
|
a0a001db71 | ||
|
|
d65ac7421d | ||
|
|
b43b542667 | ||
|
|
676c95ed2d | ||
|
|
5b0a475197 | ||
|
|
4521d59bec | ||
|
|
8f083e17f8 | ||
|
|
9a3895c79e | ||
|
|
eea7e2fa2d | ||
|
|
990e076c2c | ||
|
|
73fa76d792 | ||
|
|
6df1fae447 | ||
|
|
3417c6ad8d | ||
|
|
95592d9283 | ||
|
|
09e3bf94eb | ||
|
|
bc13c9db83 | ||
|
|
183e0543b4 | ||
|
|
d478517c51 | ||
|
|
b224fd324d | ||
|
|
8c627e2b8b | ||
|
|
c8d26d99f0 | ||
|
|
0f26ebe954 | ||
|
|
5513ffc33c | ||
|
|
6a6ea263cf | ||
|
|
1a789a05db | ||
|
|
47e31dc9ee | ||
|
|
0100f87ff2 | ||
|
|
8c78a210ef | ||
|
|
dd81af4cd5 | ||
|
|
2cfbd0dc1d | ||
|
|
a470cc212e | ||
|
|
f7b129d790 | ||
|
|
e02d5e7ff1 | ||
|
|
323992e224 | ||
|
|
f744467c5d | ||
|
|
7cbe017932 | ||
|
|
c1b0ab75e1 | ||
|
|
51c41ba4e3 | ||
|
|
51dd9b6dde | ||
|
|
d697e8e677 | ||
|
|
d5df1c070d | ||
|
|
273db75248 | ||
|
|
5ef7a8d55a | ||
|
|
2ca4bde06a | ||
|
|
4ceb13291f | ||
|
|
03a5d4e131 | ||
|
|
5de828d6e2 | ||
|
|
b84e551aea | ||
|
|
c48ef281ab | ||
|
|
b10fd172fd | ||
|
|
314582ef0c | ||
|
|
b1fd9daf5f | ||
|
|
7fe2dafa04 | ||
|
|
48619c9d7c | ||
|
|
0e1cc05189 | ||
|
|
ce9673b06d | ||
|
|
a9634199e6 | ||
|
|
cf6f916ed4 | ||
|
|
8ca45acb4e | ||
|
|
23b2ca50e2 | ||
|
|
526405c83b | ||
|
|
536424b0c8 | ||
|
|
0859e38bd5 | ||
|
|
46bbd78b23 | ||
|
|
5c474ec42d | ||
|
|
2df2f35423 | ||
|
|
216075cc72 | ||
|
|
f5fba333d9 | ||
|
|
aad14b8b87 | ||
|
|
8bcaf832ae | ||
|
|
aac01cb096 | ||
|
|
e72fefa74d | ||
|
|
7617b8af52 | ||
|
|
33fd9c7c8f | ||
|
|
8703124c76 | ||
|
|
4ee2c311a7 | ||
|
|
020593d509 | ||
|
|
1f118c4b84 | ||
|
|
abbc6a2587 | ||
|
|
37eb6c90b6 | ||
|
|
fc302186b2 | ||
|
|
903ca567c5 | ||
|
|
fd75f157d6 | ||
|
|
f86224a64e | ||
|
|
ed41252207 | ||
|
|
6ec411b4bb | ||
|
|
5a26d4c039 | ||
|
|
94950cccc8 | ||
|
|
3a00077305 | ||
|
|
4a82606ffb | ||
|
|
d5f63ebac4 | ||
|
|
dce079e711 | ||
|
|
632525f4d0 | ||
|
|
98692523bf | ||
|
|
87534692d0 | ||
|
|
a358174536 | ||
|
|
ff32f90a29 | ||
|
|
fb69620e49 | ||
|
|
d219f244d2 | ||
|
|
b0860ce5f0 | ||
|
|
56a2c587ad | ||
|
|
c43eceb2cb | ||
|
|
799e1f0469 | ||
|
|
fdcf332a8a | ||
|
|
bf3329e9a9 | ||
|
|
6b26154077 | ||
|
|
5546ecd637 | ||
|
|
079d4039a1 | ||
|
|
fc442361cd | ||
|
|
1ed0c7d85d | ||
|
|
3979387c80 | ||
|
|
f86b645417 | ||
|
|
c3ff5de016 | ||
|
|
9b15cfa5a5 | ||
|
|
7837b4893f | ||
|
|
d88edb0661 | ||
|
|
7a50450b92 | ||
|
|
3afa4726bf | ||
|
|
218e97d965 | ||
|
|
8d19192c9c | ||
|
|
179d99381d | ||
|
|
b8dfa4c3d2 | ||
|
|
92bec562ab | ||
|
|
1802c0a922 | ||
|
|
f2cc00cc64 | ||
|
|
033e06868e | ||
|
|
688d706449 | ||
|
|
028597f774 | ||
|
|
0550baaf4f | ||
|
|
c33b171043 | ||
|
|
9d67d229fa | ||
|
|
cdbf2f9293 | ||
|
|
5def6ebc3b | ||
|
|
ea62deda59 | ||
|
|
3e43f4e58e | ||
|
|
2e08766cb1 | ||
|
|
e476b9d225 | ||
|
|
b2b836d4c1 | ||
|
|
5e81736f88 | ||
|
|
37427d052e | ||
|
|
5656b0eb2c | ||
|
|
b854cdb95b | ||
|
|
be31a860d1 | ||
|
|
0409192e64 | ||
|
|
c036141b37 | ||
|
|
b8e4c2ff69 | ||
|
|
cc236529c4 | ||
|
|
dacd7cd8a4 | ||
|
|
8a301c6c59 | ||
|
|
6918993c75 | ||
|
|
88161cd5c9 | ||
|
|
4883036789 | ||
|
|
965bd8a2e0 | ||
|
|
2224d05607 | ||
|
|
ed66c21035 | ||
|
|
9384e2c963 | ||
|
|
6af42b4372 | ||
|
|
1235dae5b8 | ||
|
|
a35d05ac7e | ||
|
|
6b103cc9b9 | ||
|
|
a23f60315f | ||
|
|
3bc6a7da32 | ||
|
|
dc5b610ee5 | ||
|
|
7723db9e62 | ||
|
|
cf612c3d5b | ||
|
|
d2b6660881 | ||
|
|
3972d1d4c6 | ||
|
|
5fda78cf91 | ||
|
|
60ce2b343d | ||
|
|
6cc285aea5 | ||
|
|
c5d5d57e9b | ||
|
|
02979db3d6 | ||
|
|
0d06e8c1c9 | ||
|
|
d793cfeb68 | ||
|
|
382a62346b | ||
|
|
9e0ca719ed | ||
|
|
88b70e964c | ||
|
|
4dda842b16 | ||
|
|
3cba09c6f6 | ||
|
|
c4bc42d527 | ||
|
|
cf04a81f70 | ||
|
|
92014bf1d1 | ||
|
|
cba55402b1 | ||
|
|
f56b3d8e9c | ||
|
|
8313225b40 | ||
|
|
13042d5557 | ||
|
|
85858885c8 | ||
|
|
10f48fbf6b | ||
|
|
8b267e3faf | ||
|
|
13effee679 | ||
|
|
903cda08b1 | ||
|
|
e0e2f739ba | ||
|
|
0e710099e0 | ||
|
|
b1b0a2589e | ||
|
|
dff36b8087 | ||
|
|
46ad194097 | ||
|
|
2ba9d825a0 | ||
|
|
8c0035c5b3 | ||
|
|
c53fc94e84 | ||
|
|
bccd880395 | ||
|
|
8267a21bfe | ||
|
|
3cbd77f6ac | ||
|
|
4a6b5ba02b | ||
|
|
efb83dde19 | ||
|
|
c20324793e | ||
|
|
4496ee5af0 | ||
|
|
e42c4859c2 | ||
|
|
939d1b5ff6 | ||
|
|
5c2cecde70 | ||
|
|
c613585100 | ||
|
|
3fbf09e7d9 | ||
|
|
b22a26891e | ||
|
|
a0df165011 | ||
|
|
f6307a1523 | ||
|
|
ff9688bb7a | ||
|
|
c714658384 | ||
|
|
f21914d1f3 | ||
|
|
417c193c0d | ||
|
|
455c629f47 | ||
|
|
01896786fb | ||
|
|
c526fcd40f | ||
|
|
8b57777ce9 | ||
|
|
b61197196e | ||
|
|
e0a1b87296 | ||
|
|
d3ed6d5242 | ||
|
|
5d6455415e | ||
|
|
a27b37be59 | ||
|
|
f6a67d99e4 | ||
|
|
486263fff7 | ||
|
|
5a469f4d4b | ||
|
|
889eef78e4 | ||
|
|
794cfb7976 | ||
|
|
9055922153 | ||
|
|
2330268842 | ||
|
|
e81d17dae0 | ||
|
|
5820bab9b1 | ||
|
|
3a00c3de64 | ||
|
|
71fb7a6ef6 | ||
|
|
65d841c3a6 | ||
|
|
5236b720ab | ||
|
|
ebbdce1f42 | ||
|
|
1490ccf7fb | ||
|
|
642e9c53ba | ||
|
|
1e9e6927eb | ||
|
|
b7c7041873 | ||
|
|
35f35050ff | ||
|
|
6cb02128b4 | ||
|
|
3436676de2 | ||
|
|
04de22613c | ||
|
|
2cf5acdfd2 | ||
|
|
4512a972d1 | ||
|
|
65df6f0013 | ||
|
|
eb00e54eba | ||
|
|
ec700c2cd6 | ||
|
|
75a39352ff | ||
|
|
36c7fbe06a | ||
|
|
bb2191b2b0 | ||
|
|
f314b6cb6c | ||
|
|
86e1d0f952 | ||
|
|
02c3ea1917 | ||
|
|
0a67195529 | ||
|
|
feb70b47df | ||
|
|
909f613324 | ||
|
|
f0bf7b0def | ||
|
|
541707c3e7 | ||
|
|
add89d6973 | ||
|
|
e2cec9b3ae | ||
|
|
0f914b4c20 | ||
|
|
1b4be0460c | ||
|
|
cb899a9465 | ||
|
|
b362017206 | ||
|
|
6e63a4ed8a | ||
|
|
b9c852392c | ||
|
|
976a0fe38c | ||
|
|
fc8b25a71f | ||
|
|
fcbf7abdaa | ||
|
|
7759ab6919 | ||
|
|
3fd620198e | ||
|
|
49bc95549b | ||
|
|
ba0f7a4101 | ||
|
|
4914ad1dd9 | ||
|
|
f07a4684e0 | ||
|
|
cfd78f7b02 | ||
|
|
2a2e6b6334 | ||
|
|
b635637541 | ||
|
|
a7ebba6863 | ||
|
|
1f8acb49bc | ||
|
|
5a4bca9780 | ||
|
|
d7e52d8014 | ||
|
|
391a8901c8 | ||
|
|
b98e03b5bc | ||
|
|
6b586c268a | ||
|
|
9a34e7174c | ||
|
|
3203849b60 | ||
|
|
2e582a4597 | ||
|
|
5d38eec37d | ||
|
|
a6c7fe04da | ||
|
|
966ab20f26 | ||
|
|
b2e9dc5c8f | ||
|
|
fc3a8e4e79 | ||
|
|
00352d41a7 | ||
|
|
29c26e0015 | ||
|
|
63d9bd4a9c | ||
|
|
cb23549af6 | ||
|
|
e5cc5a58e1 | ||
|
|
aa8db784d5 | ||
|
|
d68d4d1129 | ||
|
|
8d32e883bd | ||
|
|
7826b9aa72 | ||
|
|
e91d47db96 | ||
|
|
e92e433805 | ||
|
|
40e1d35268 | ||
|
|
af6c39f4d1 | ||
|
|
5be949f00f | ||
|
|
169459b57f | ||
|
|
05926b1994 | ||
|
|
54b4142530 | ||
|
|
f0244d7982 | ||
|
|
7a600ea064 | ||
|
|
d687bc073e | ||
|
|
a59b02b6b4 | ||
|
|
14919082a3 | ||
|
|
802a95eac5 | ||
|
|
a44181fd35 | ||
|
|
b280a791a6 | ||
|
|
7269070d97 | ||
|
|
94ac0b5ed8 | ||
|
|
8ef8dbc868 | ||
|
|
7de3c62846 | ||
|
|
fb90dab471 | ||
|
|
419ec7f7a7 | ||
|
|
bdf64ccbbb | ||
|
|
3fa45375d9 | ||
|
|
5566ea8c81 | ||
|
|
8c303bf48c | ||
|
|
8683d75aa1 | ||
|
|
4f5d7cea11 | ||
|
|
ab9ffc4f05 | ||
|
|
6e2bfcfe65 | ||
|
|
240098dd7e | ||
|
|
353bb62687 | ||
|
|
46df91ff45 | ||
|
|
295caeb065 | ||
|
|
eeb309aea1 | ||
|
|
9e0a765801 | ||
|
|
c94cc34a8f | ||
|
|
901d4b5489 | ||
|
|
2e4e3a42cc | ||
|
|
b866687cd7 | ||
|
|
7faa94046c | ||
|
|
1d579587c1 | ||
|
|
b28bfad496 | ||
|
|
e0682044f0 | ||
|
|
81f1a65fae | ||
|
|
8efc4b5ba9 | ||
|
|
b4e2537de3 | ||
|
|
90e25a6dfb | ||
|
|
3d90855ca6 | ||
|
|
200c927087 | ||
|
|
ef22a6e18d | ||
|
|
061395d2f8 | ||
|
|
05258ea4bf | ||
|
|
8742ce035a | ||
|
|
0664bf31a2 | ||
|
|
0ec1ff642d | ||
|
|
cd44824233 | ||
|
|
79240a0d47 | ||
|
|
c03d04d826 | ||
|
|
dfb8b5a3c1 | ||
|
|
432304be82 | ||
|
|
294d8171a2 | ||
|
|
a46ddcf6dd | ||
|
|
5ca006cc9c | ||
|
|
564ed26aeb | ||
|
|
5860267410 | ||
|
|
ec9638f4d1 | ||
|
|
7db8bbf385 | ||
|
|
024f1d4882 | ||
|
|
3375261f51 | ||
|
|
640d58f0a8 | ||
|
|
793b8b8ad3 | ||
|
|
39af43eb5c | ||
|
|
b56675a7bb | ||
|
|
c4d71e934d | ||
|
|
f4d7bbd044 | ||
|
|
3d5d90241f | ||
|
|
a7c8e202aa | ||
|
|
a63658d583 | ||
|
|
ca81180e6d | ||
|
|
de4c8adca2 | ||
|
|
823e260c2a | ||
|
|
1c8b5838cd | ||
|
|
3473ef63af | ||
|
|
2cced1dac3 | ||
|
|
b5d3a4736b | ||
|
|
e627544479 | ||
|
|
d547345f90 | ||
|
|
4ec3289f9c | ||
|
|
638dd25aff | ||
|
|
37efd5a5cd | ||
|
|
168065b9bc | ||
|
|
95cd2035b6 | ||
|
|
aeba81e193 | ||
|
|
c7e327ea87 | ||
|
|
ed06b8cead | ||
|
|
a79c7ee217 | ||
|
|
6bf23f9167 | ||
|
|
1b3963299d | ||
|
|
c461a7c7e2 | ||
|
|
0cfff13be1 | ||
|
|
0245189670 | ||
|
|
7777d5811f | ||
|
|
7259cc878e | ||
|
|
a4214afddb | ||
|
|
b078f6c342 | ||
|
|
81974885ee | ||
|
|
b2c5a9f5fe | ||
|
|
04cb893d10 | ||
|
|
3b228c78c0 | ||
|
|
4e91e6d103 | ||
|
|
cb4e886a4f | ||
|
|
bee80c5b79 | ||
|
|
4479761131 | ||
|
|
20f1e1609f | ||
|
|
1f1115f631 | ||
|
|
f7c2ec19ef | ||
|
|
fed7bd9473 | ||
|
|
4d6070e33a | ||
|
|
0a7e6ac222 | ||
|
|
f892c3394b | ||
|
|
929d49ed6f | ||
|
|
3c1f8cd882 | ||
|
|
f21da7cfdc | ||
|
|
39d33c97ff | ||
|
|
3193e825d5 | ||
|
|
c262a387dc | ||
|
|
36d5fff8e0 | ||
|
|
72aa722c33 | ||
|
|
c952f2e18a | ||
|
|
0fc7f37185 | ||
|
|
9cff6c7e6a | ||
|
|
e66268dffe | ||
|
|
c13b510ba3 | ||
|
|
5f4baa67dc | ||
|
|
1db7e2c9d6 | ||
|
|
fa324dce9c | ||
|
|
56c694b477 | ||
|
|
3f764f1981 | ||
|
|
63d6734612 | ||
|
|
f9743c29cd | ||
|
|
bdb7a29586 | ||
|
|
22c36f0ad3 | ||
|
|
fd6373c7aa | ||
|
|
87fe674c70 | ||
|
|
ddec566e10 | ||
|
|
454d8535f8 | ||
|
|
8e4942088e | ||
|
|
3af527b1b5 | ||
|
|
69d5738e47 | ||
|
|
379c10985b | ||
|
|
821cf7135d | ||
|
|
d986bdab98 | ||
|
|
53d9fd18b7 | ||
|
|
38a1f06d14 | ||
|
|
2e2d0f48fb | ||
|
|
4652b8aea1 | ||
|
|
ef1cbd3aea | ||
|
|
31cedf83c7 | ||
|
|
19a97580fc | ||
|
|
17f3cf0389 | ||
|
|
4e02300cbc | ||
|
|
015cdd155c | ||
|
|
d4e603cc6a | ||
|
|
7ae374e11f | ||
|
|
292b403dc3 | ||
|
|
b815898ddb | ||
|
|
b1855f1d1d | ||
|
|
bd6a17a3a5 | ||
|
|
29fad3fa3c | ||
|
|
0c43466225 | ||
|
|
0d6c95ac44 | ||
|
|
6776e942d7 | ||
|
|
879e32f670 | ||
|
|
3a246df544 | ||
|
|
9577525b0b | ||
|
|
6b410d8076 | ||
|
|
9e82433a3e | ||
|
|
8ceaa72ba3 | ||
|
|
fce994ea76 | ||
|
|
4390fed168 | ||
|
|
0f8e48c26d | ||
|
|
2d556486bf | ||
|
|
850a20a626 | ||
|
|
68dc0d4d99 | ||
|
|
58e66c947b | ||
|
|
29f47d58bc | ||
|
|
8947052405 | ||
|
|
475b7896e2 | ||
|
|
b2a2cb3fd8 | ||
|
|
462a438f89 | ||
|
|
4ebc52ab52 | ||
|
|
8afeef2f36 | ||
|
|
c2525782aa | ||
|
|
bc4de4e769 | ||
|
|
9f324205cb | ||
|
|
fff85ab392 | ||
|
|
29f4b73230 | ||
|
|
606fa34792 | ||
|
|
7b452208b6 | ||
|
|
493de295ac | ||
|
|
d2106c40e1 | ||
|
|
9a0a5b7867 | ||
|
|
d8003c4d87 | ||
|
|
f7380dc927 | ||
|
|
ea6ca9252c | ||
|
|
bfc61c268a | ||
|
|
1c227bc0d9 | ||
|
|
bb870a688d | ||
|
|
40a98d56fa | ||
|
|
373508693a | ||
|
|
59fa4f18e4 | ||
|
|
253d5aea6e | ||
|
|
99ea2c17a1 | ||
|
|
7ab15c0e79 | ||
|
|
4e4d4365a0 | ||
|
|
1f82bb033d | ||
|
|
cadd797200 | ||
|
|
6df5e712f7 | ||
|
|
282e37ef14 | ||
|
|
0668fba7bd | ||
|
|
27270b49b4 | ||
|
|
8c5d6ee9c3 | ||
|
|
934c19445d | ||
|
|
72251e0375 | ||
|
|
b1e2275b47 | ||
|
|
af1bde6619 | ||
|
|
2daea92379 | ||
|
|
6cd9ca018a | ||
|
|
eb282b3bb3 | ||
|
|
fe0a9529ed | ||
|
|
1b7a64412d | ||
|
|
a187bd5455 | ||
|
|
3e962808e6 | ||
|
|
3d5a9b5e91 | ||
|
|
ba43218a73 | ||
|
|
d8bf15a2f5 | ||
|
|
dbbbe1ceef | ||
|
|
2817f03378 | ||
|
|
fcc164c31e | ||
|
|
65d5b64d8d | ||
|
|
f6547ec157 | ||
|
|
61cddaa441 | ||
|
|
b03c024f74 | ||
|
|
1a7522a594 | ||
|
|
d0b9f08bf2 | ||
|
|
3dd49b2b95 | ||
|
|
3ef9c99003 | ||
|
|
47183ce02e | ||
|
|
f2dea4615f | ||
|
|
b4635db5ac | ||
|
|
b668b19543 | ||
|
|
cfb1853bbd | ||
|
|
b784d80973 | ||
|
|
2084ad2164 | ||
|
|
9c77f5f5a9 | ||
|
|
8a750eba68 | ||
|
|
5dbd554a10 | ||
|
|
d0296561f6 | ||
|
|
db212cfb00 | ||
|
|
6db5afe597 | ||
|
|
2ba83655bb | ||
|
|
6e27e73474 | ||
|
|
f0fe8cb2fe | ||
|
|
efd45549e4 | ||
|
|
34a4db57db | ||
|
|
e62ef067cc | ||
|
|
df37cb11fa | ||
|
|
857d6b5b49 | ||
|
|
62a740ba22 | ||
|
|
a83e741dc7 | ||
|
|
7695ca2c8b | ||
|
|
3f5c748560 | ||
|
|
fb32cc39e1 | ||
|
|
b548116f9b | ||
|
|
2031b2803f | ||
|
|
50775ce509 | ||
|
|
709df1e844 | ||
|
|
09d826edf4 | ||
|
|
086f64b06c | ||
|
|
6ad62a2ccb | ||
|
|
92fe9aadc8 | ||
|
|
a9feafd571 | ||
|
|
e1d1cf76ca | ||
|
|
1317297191 | ||
|
|
64a393b377 | ||
|
|
3ad64b0a66 | ||
|
|
2664ca498e | ||
|
|
5b44e83c0f | ||
|
|
b8b4e32758 | ||
|
|
2b60fca08d | ||
|
|
f43092c563 | ||
|
|
68d2076b56 | ||
|
|
be5f0fb3ac | ||
|
|
e9b691173a | ||
|
|
2a77883146 | ||
|
|
a3c6211c04 | ||
|
|
afcb0b8767 | ||
|
|
c0c439c549 | ||
|
|
959f6386b4 | ||
|
|
f1fe8e95ba | ||
|
|
6892033556 | ||
|
|
766893253a | ||
|
|
ef5edb95ba | ||
|
|
f21b9988e9 | ||
|
|
7859b76429 | ||
|
|
2c44e4fb12 | ||
|
|
9b373901fa | ||
|
|
37be81c20c | ||
|
|
5fe2db228c | ||
|
|
c91d52a587 |
83
.coveragerc
@@ -11,6 +11,9 @@ omit =
|
||||
homeassistant/components/abode.py
|
||||
homeassistant/components/*/abode.py
|
||||
|
||||
homeassistant/components/ads/__init__.py
|
||||
homeassistant/components/*/ads.py
|
||||
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
@@ -47,12 +50,20 @@ omit =
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
homeassistant/components/coinbase.py
|
||||
homeassistant/components/sensor/coinbase.py
|
||||
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
homeassistant/components/deconz/*
|
||||
homeassistant/components/*/deconz.py
|
||||
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
homeassistant/components/dominos.py
|
||||
|
||||
homeassistant/components/doorbird.py
|
||||
homeassistant/components/*/doorbird.py
|
||||
|
||||
@@ -80,9 +91,15 @@ omit =
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/*/hdmi_cec.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/hive.py
|
||||
homeassistant/components/*/hive.py
|
||||
|
||||
homeassistant/components/homematic/__init__.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/*/ihc.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
homeassistant/components/*/insteon_local.py
|
||||
|
||||
@@ -92,6 +109,9 @@ omit =
|
||||
homeassistant/components/ios.py
|
||||
homeassistant/components/*/ios.py
|
||||
|
||||
homeassistant/components/iota.py
|
||||
homeassistant/components/*/iota.py
|
||||
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
@@ -131,6 +151,9 @@ omit =
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
homeassistant/components/mychevy.py
|
||||
homeassistant/components/*/mychevy.py
|
||||
|
||||
homeassistant/components/mysensors.py
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
@@ -182,6 +205,9 @@ omit =
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/tahoma.py
|
||||
homeassistant/components/*/tahoma.py
|
||||
|
||||
homeassistant/components/tellduslive.py
|
||||
homeassistant/components/*/tellduslive.py
|
||||
|
||||
@@ -224,6 +250,9 @@ omit =
|
||||
homeassistant/components/volvooncall.py
|
||||
homeassistant/components/*/volvooncall.py
|
||||
|
||||
homeassistant/components/waterfurnace.py
|
||||
homeassistant/components/*/waterfurnace.py
|
||||
|
||||
homeassistant/components/*/webostv.py
|
||||
|
||||
homeassistant/components/wemo.py
|
||||
@@ -252,9 +281,14 @@ omit =
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/daikin.py
|
||||
homeassistant/components/*/daikin.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/egardia.py
|
||||
homeassistant/components/alarm_control_panel/ialarm.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
@@ -271,26 +305,33 @@ omit =
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/calendar/caldav.py
|
||||
homeassistant/components/calendar/todoist.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/canary.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/ring.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/camera/xeoma.py
|
||||
homeassistant/components/camera/yi.py
|
||||
homeassistant/components/climate/econet.py
|
||||
homeassistant/components/climate/ephember.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/flexit.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/honeywell.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/climate/sensibo.py
|
||||
homeassistant/components/climate/touchline.py
|
||||
homeassistant/components/climate/venstar.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/knx.py
|
||||
@@ -300,7 +341,6 @@ omit =
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/automatic.py
|
||||
homeassistant/components/device_tracker/bbox.py
|
||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||
@@ -323,10 +363,10 @@ omit =
|
||||
homeassistant/components/device_tracker/sky_hub.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tado.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tile.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
@@ -344,20 +384,22 @@ omit =
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/light/avion.py
|
||||
homeassistant/components/light/blinkt.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/blinkt.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/decora_wifi.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/greenwave.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/iglo.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
homeassistant/components/light/sensehat.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/tplink.py
|
||||
@@ -368,9 +410,9 @@ omit =
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/nello.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/media_extractor.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
@@ -412,11 +454,13 @@ omit =
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/ue_smart_radio.py
|
||||
homeassistant/components/media_player/vizio.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/media_player/yamaha_musiccast.py
|
||||
homeassistant/components/media_player/ziggo_mediabox_xl.py
|
||||
homeassistant/components/mycroft.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
@@ -426,7 +470,6 @@ omit =
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
@@ -459,12 +502,14 @@ omit =
|
||||
homeassistant/components/notify/yessssms.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/prometheus.py
|
||||
homeassistant/components/rainbird.py
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
homeassistant/components/sensor/alpha_vantage.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
@@ -472,11 +517,12 @@ omit =
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/blockchain.py
|
||||
homeassistant/components/sensor/bme280.py
|
||||
homeassistant/components/sensor/bme680.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/crimereports.py
|
||||
@@ -486,6 +532,7 @@ omit =
|
||||
homeassistant/components/sensor/deluge.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/discogs.py
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
@@ -499,11 +546,11 @@ omit =
|
||||
homeassistant/components/sensor/etherscan.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/fido.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gearbest.py
|
||||
homeassistant/components/sensor/geizhals.py
|
||||
homeassistant/components/sensor/gitter.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
@@ -513,7 +560,6 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/hydroquebec.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
@@ -551,6 +597,7 @@ omit =
|
||||
homeassistant/components/sensor/pyload.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
@@ -561,6 +608,7 @@ omit =
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
homeassistant/components/sensor/sma.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sochain.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
@@ -572,6 +620,7 @@ omit =
|
||||
homeassistant/components/sensor/sytadin.py
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/teksavvy.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/tibber.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
@@ -589,7 +638,6 @@ omit =
|
||||
homeassistant/components/sensor/worldtidesinfo.py
|
||||
homeassistant/components/sensor/worxlandroid.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/shiftr.py
|
||||
homeassistant/components/spc.py
|
||||
@@ -615,26 +663,27 @@ omit =
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/snmp.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/xiaomi_miio.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
homeassistant/components/tts/baidu.py
|
||||
homeassistant/components/tts/microsoft.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
homeassistant/components/vacuum/roomba.py
|
||||
homeassistant/components/vacuum/xiaomi_miio.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
homeassistant/components/weather/darksky.py
|
||||
homeassistant/components/weather/metoffice.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/yweather.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/util.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
|
||||
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ensure Docker script files uses LF to support Docker for Windows.
|
||||
setup_docker_prereqs eol=lf
|
||||
/virtualization/Docker/scripts/* eol=lf
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -11,6 +11,7 @@
|
||||
```
|
||||
|
||||
## Checklist:
|
||||
- [ ] The code change is tested and works locally.
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
|
||||
13
.github/move.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Configuration for move-issues - https://github.com/dessant/move-issues
|
||||
|
||||
# Delete the command comment. Ignored when the comment also contains other content
|
||||
deleteCommand: true
|
||||
# Close the source issue after moving
|
||||
closeSourceIssue: true
|
||||
# Lock the source issue after moving
|
||||
lockSourceIssue: false
|
||||
# Set custom aliases for targets
|
||||
# aliases:
|
||||
# r: repo
|
||||
# or: owner/repo
|
||||
|
||||
3
.gitignore
vendored
@@ -74,6 +74,7 @@ pip-selfcheck.json
|
||||
venv
|
||||
.venv
|
||||
Pipfile*
|
||||
share/*
|
||||
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
@@ -96,4 +97,4 @@ docs/build
|
||||
desktop.ini
|
||||
/home-assistant.pyproj
|
||||
/home-assistant.sln
|
||||
/.vs/home-assistant/v14
|
||||
/.vs/*
|
||||
|
||||
@@ -8,18 +8,18 @@ matrix:
|
||||
include:
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=lint
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=pylint
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=py34
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5"
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
# - python: "3.6-dev"
|
||||
# env: TOXENV=py36
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
@@ -29,5 +29,5 @@ cache:
|
||||
- $HOME/.cache/pip
|
||||
install: pip install -U tox coveralls
|
||||
language: python
|
||||
script: travis_wait tox
|
||||
script: travis_wait 30 tox --develop
|
||||
after_success: coveralls
|
||||
|
||||
16
CODEOWNERS
@@ -32,12 +32,15 @@ homeassistant/components/zone.py @home-assistant/core
|
||||
# To monitor non-pypi additions
|
||||
requirements_all.txt @andrey-git
|
||||
|
||||
# HomeAssistant developer Teams
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
|
||||
homeassistant/components/hassio.py @home-assistant/hassio
|
||||
|
||||
# Indiviudal components
|
||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
@@ -46,15 +49,18 @@ homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/plant.py @ChristianKuehnel
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
@@ -62,14 +68,22 @@ homeassistant/components/switch/rainmachine.py @bachya
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
homeassistant/components/*/axis.py @kane610
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tradfri.py @ggravlingen
|
||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||
|
||||
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
@@ -230,7 +230,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
||||
|
||||
def cmdline() -> List[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if sys.argv[0].endswith(os.path.sep + '__main__.py'):
|
||||
if os.path.basename(sys.argv[0]) == '__main__.py':
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
|
||||
@@ -10,7 +10,6 @@ Component design guidelines:
|
||||
import asyncio
|
||||
import itertools as it
|
||||
import logging
|
||||
import os
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
@@ -111,11 +110,6 @@ def async_reload_core_config(hass):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up general services related to Home Assistant."""
|
||||
descriptions = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_turn_service(service):
|
||||
"""Handle calls to homeassistant.turn_on/off."""
|
||||
@@ -155,14 +149,11 @@ def async_setup(hass, config):
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TURN_OFF])
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TURN_ON])
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TOGGLE])
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_core_service(call):
|
||||
@@ -187,14 +178,11 @@ def async_setup(hass, config):
|
||||
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_STOP])
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_RESTART])
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_CHECK_CONFIG])
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_reload_config(call):
|
||||
@@ -209,7 +197,6 @@ def async_setup(hass, config):
|
||||
hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config,
|
||||
descriptions[ha.DOMAIN][SERVICE_RELOAD_CORE_CONFIG])
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -7,11 +7,9 @@ https://home-assistant.io/components/abode/
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import partial
|
||||
from os import path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME,
|
||||
CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS,
|
||||
@@ -188,22 +186,16 @@ def setup_hass_services(hass):
|
||||
for device in target_devices:
|
||||
device.trigger()
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))[DOMAIN]
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SETTINGS, change_setting,
|
||||
descriptions.get(SERVICE_SETTINGS),
|
||||
schema=CHANGE_SETTING_SCHEMA)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image,
|
||||
descriptions.get(SERVICE_CAPTURE_IMAGE),
|
||||
schema=CAPTURE_IMAGE_SCHEMA)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_quick_action,
|
||||
descriptions.get(SERVICE_TRIGGER),
|
||||
schema=TRIGGER_SCHEMA)
|
||||
|
||||
|
||||
|
||||
196
homeassistant/components/ads/__init__.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
Support for Automation Device Specification (ADS).
|
||||
|
||||
For more details about this component, please refer to the documentation.
|
||||
https://home-assistant.io/components/ads/
|
||||
"""
|
||||
import threading
|
||||
import struct
|
||||
import logging
|
||||
import ctypes
|
||||
from collections import namedtuple
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
|
||||
EVENT_HOMEASSISTANT_STOP
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyads==2.2.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_ADS = 'data_ads'
|
||||
|
||||
# Supported Types
|
||||
ADSTYPE_INT = 'int'
|
||||
ADSTYPE_UINT = 'uint'
|
||||
ADSTYPE_BYTE = 'byte'
|
||||
ADSTYPE_BOOL = 'bool'
|
||||
|
||||
DOMAIN = 'ads'
|
||||
|
||||
CONF_ADS_VAR = 'adsvar'
|
||||
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
|
||||
CONF_ADS_TYPE = 'adstype'
|
||||
CONF_ADS_FACTOR = 'factor'
|
||||
CONF_ADS_VALUE = 'value'
|
||||
|
||||
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_IP_ADDRESS): cv.string,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
|
||||
vol.Required(CONF_ADS_TYPE):
|
||||
vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]),
|
||||
vol.Required(CONF_ADS_VALUE): cv.match_all,
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the ADS component."""
|
||||
import pyads
|
||||
conf = config[DOMAIN]
|
||||
|
||||
net_id = conf.get(CONF_DEVICE)
|
||||
ip_address = conf.get(CONF_IP_ADDRESS)
|
||||
port = conf.get(CONF_PORT)
|
||||
|
||||
client = pyads.Connection(net_id, port, ip_address)
|
||||
|
||||
AdsHub.ADS_TYPEMAP = {
|
||||
ADSTYPE_BOOL: pyads.PLCTYPE_BOOL,
|
||||
ADSTYPE_BYTE: pyads.PLCTYPE_BYTE,
|
||||
ADSTYPE_INT: pyads.PLCTYPE_INT,
|
||||
ADSTYPE_UINT: pyads.PLCTYPE_UINT,
|
||||
}
|
||||
|
||||
AdsHub.PLCTYPE_BOOL = pyads.PLCTYPE_BOOL
|
||||
AdsHub.PLCTYPE_BYTE = pyads.PLCTYPE_BYTE
|
||||
AdsHub.PLCTYPE_INT = pyads.PLCTYPE_INT
|
||||
AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT
|
||||
AdsHub.ADSError = pyads.ADSError
|
||||
|
||||
try:
|
||||
ads = AdsHub(client)
|
||||
except pyads.pyads.ADSError:
|
||||
_LOGGER.error(
|
||||
"Could not connect to ADS host (netid=%s, port=%s)", net_id, port)
|
||||
return False
|
||||
|
||||
hass.data[DATA_ADS] = ads
|
||||
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, ads.shutdown)
|
||||
|
||||
def handle_write_data_by_name(call):
|
||||
"""Write a value to the connected ADS device."""
|
||||
ads_var = call.data.get(CONF_ADS_VAR)
|
||||
ads_type = call.data.get(CONF_ADS_TYPE)
|
||||
value = call.data.get(CONF_ADS_VALUE)
|
||||
|
||||
try:
|
||||
ads.write_by_name(ads_var, value, ads.ADS_TYPEMAP[ads_type])
|
||||
except pyads.ADSError as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
|
||||
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Tuple to hold data needed for notification
|
||||
NotificationItem = namedtuple(
|
||||
'NotificationItem', 'hnotify huser name plc_datatype callback'
|
||||
)
|
||||
|
||||
|
||||
class AdsHub(object):
|
||||
"""Representation of an ADS connection."""
|
||||
|
||||
def __init__(self, ads_client):
|
||||
"""Initialize the ADS hub."""
|
||||
self._client = ads_client
|
||||
self._client.open()
|
||||
|
||||
# All ADS devices are registered here
|
||||
self._devices = []
|
||||
self._notification_items = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def shutdown(self, *args, **kwargs):
|
||||
"""Shutdown ADS connection."""
|
||||
_LOGGER.debug("Shutting down ADS")
|
||||
for notification_item in self._notification_items.values():
|
||||
self._client.del_device_notification(
|
||||
notification_item.hnotify,
|
||||
notification_item.huser
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Deleting device notification %d, %d",
|
||||
notification_item.hnotify, notification_item.huser)
|
||||
self._client.close()
|
||||
|
||||
def register_device(self, device):
|
||||
"""Register a new device."""
|
||||
self._devices.append(device)
|
||||
|
||||
def write_by_name(self, name, value, plc_datatype):
|
||||
"""Write a value to the device."""
|
||||
with self._lock:
|
||||
return self._client.write_by_name(name, value, plc_datatype)
|
||||
|
||||
def read_by_name(self, name, plc_datatype):
|
||||
"""Read a value from the device."""
|
||||
with self._lock:
|
||||
return self._client.read_by_name(name, plc_datatype)
|
||||
|
||||
def add_device_notification(self, name, plc_datatype, callback):
|
||||
"""Add a notification to the ADS devices."""
|
||||
from pyads import NotificationAttrib
|
||||
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
|
||||
|
||||
with self._lock:
|
||||
hnotify, huser = self._client.add_device_notification(
|
||||
name, attr, self._device_notification_callback)
|
||||
hnotify = int(hnotify)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Added device notification %d for variable %s", hnotify, name)
|
||||
|
||||
self._notification_items[hnotify] = NotificationItem(
|
||||
hnotify, huser, name, plc_datatype, callback)
|
||||
|
||||
def _device_notification_callback(self, addr, notification, huser):
|
||||
"""Handle device notifications."""
|
||||
contents = notification.contents
|
||||
|
||||
hnotify = int(contents.hNotification)
|
||||
_LOGGER.debug("Received notification %d", hnotify)
|
||||
data = contents.data
|
||||
|
||||
try:
|
||||
notification_item = self._notification_items[hnotify]
|
||||
except KeyError:
|
||||
_LOGGER.debug("Unknown device notification handle: %d", hnotify)
|
||||
return
|
||||
|
||||
# Parse data to desired datatype
|
||||
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
|
||||
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_INT:
|
||||
value = struct.unpack('<h', bytearray(data)[:2])[0]
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
|
||||
value = struct.unpack('<B', bytearray(data)[:1])[0]
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
|
||||
value = struct.unpack('<H', bytearray(data)[:2])[0]
|
||||
else:
|
||||
value = bytearray(data)
|
||||
_LOGGER.warning("No callback available for this datatype")
|
||||
|
||||
notification_item.callback(notification_item.name, value)
|
||||
15
homeassistant/components/ads/services.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Describes the format for available ADS services
|
||||
|
||||
write_data_by_name:
|
||||
description: Write a value to the connected ADS device.
|
||||
|
||||
fields:
|
||||
adsvar:
|
||||
description: The name of the variable to write to.
|
||||
example: '.global_var'
|
||||
adstype:
|
||||
description: The data type of the variable to write to.
|
||||
example: 'int'
|
||||
value:
|
||||
description: The value to write to the variable.
|
||||
example: 1
|
||||
@@ -7,15 +7,13 @@ https://home-assistant.io/components/alarm_control_panel/
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_NIGHT)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -33,6 +31,7 @@ SERVICE_TO_METHOD = {
|
||||
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
|
||||
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
|
||||
SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night',
|
||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS: 'alarm_arm_custom_bypass',
|
||||
SERVICE_ALARM_TRIGGER: 'alarm_trigger'
|
||||
}
|
||||
|
||||
@@ -107,6 +106,18 @@ def alarm_trigger(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm custom bypass."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
@@ -135,14 +146,10 @@ def async_setup(hass, config):
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_alarm_service_handler,
|
||||
descriptions.get(service), schema=ALARM_SERVICE_SCHEMA)
|
||||
schema=ALARM_SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -216,6 +223,17 @@ class AlarmControlPanel(Entity):
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_trigger, code)
|
||||
|
||||
def alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_custom_bypass, code)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
||||
@@ -6,12 +6,12 @@ https://home-assistant.io/components/alarm_control_panel.abode/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import (
|
||||
AbodeDevice, DOMAIN as ABODE_DOMAIN, CONF_ATTRIBUTION)
|
||||
from homeassistant.components.alarm_control_panel import (AlarmControlPanel)
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
|
||||
from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice
|
||||
from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
||||
@@ -21,7 +21,7 @@ ICON = 'mdi:security'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for an Abode device."""
|
||||
"""Set up an alarm control panel for an Abode device."""
|
||||
data = hass.data[ABODE_DOMAIN]
|
||||
|
||||
alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)]
|
||||
@@ -41,7 +41,7 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
"""Return the icon."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
@@ -81,5 +81,5 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel):
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._device.device_id,
|
||||
'battery_backup': self._device.battery,
|
||||
'cellular_backup': self._device.is_cellular
|
||||
'cellular_backup': self._device.is_cellular,
|
||||
}
|
||||
|
||||
@@ -7,69 +7,86 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.components.alarmdecoder import (DATA_AD,
|
||||
SIGNAL_PANEL_MESSAGE)
|
||||
|
||||
from homeassistant.components.alarmdecoder import DATA_AD, SIGNAL_PANEL_MESSAGE
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime'
|
||||
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up for AlarmDecoder alarm panels."""
|
||||
_LOGGER.debug("AlarmDecoderAlarmPanel: setup")
|
||||
device = AlarmDecoderAlarmPanel()
|
||||
add_devices([device])
|
||||
|
||||
device = AlarmDecoderAlarmPanel("Alarm Panel", hass)
|
||||
def alarm_toggle_chime_handler(service):
|
||||
"""Register toggle chime handler."""
|
||||
code = service.data.get(ATTR_CODE)
|
||||
device.alarm_toggle_chime(code)
|
||||
|
||||
async_add_devices([device])
|
||||
|
||||
return True
|
||||
hass.services.register(
|
||||
alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler,
|
||||
schema=ALARM_TOGGLE_CHIME_SCHEMA)
|
||||
|
||||
|
||||
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
def __init__(self, name, hass):
|
||||
def __init__(self):
|
||||
"""Initialize the alarm panel."""
|
||||
self._display = ""
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
_LOGGER.debug("Setting up panel")
|
||||
self._name = "Alarm Panel"
|
||||
self._state = None
|
||||
self._ac_power = None
|
||||
self._backlight_on = None
|
||||
self._battery_low = None
|
||||
self._check_zone = None
|
||||
self._chime = None
|
||||
self._entry_delay_off = None
|
||||
self._programming_mode = None
|
||||
self._ready = None
|
||||
self._zone_bypassed = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
|
||||
@callback
|
||||
def _message_callback(self, message):
|
||||
"""Handle received messages."""
|
||||
if message.alarm_sounding or message.fire_alarm:
|
||||
if self._state != STATE_ALARM_TRIGGERED:
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self.async_schedule_update_ha_state()
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
elif message.armed_away:
|
||||
if self._state != STATE_ALARM_ARMED_AWAY:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self.async_schedule_update_ha_state()
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif message.armed_home:
|
||||
if self._state != STATE_ALARM_ARMED_HOME:
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self.async_schedule_update_ha_state()
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
if self._state != STATE_ALARM_DISARMED:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self.async_schedule_update_ha_state()
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
|
||||
self._ac_power = message.ac_power
|
||||
self._backlight_on = message.backlight_on
|
||||
self._battery_low = message.battery_low
|
||||
self._check_zone = message.check_zone
|
||||
self._chime = message.chime_on
|
||||
self._entry_delay_off = message.entry_delay_off
|
||||
self._programming_mode = message.programming_mode
|
||||
self._ready = message.ready
|
||||
self._zone_bypassed = message.zone_bypassed
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -91,26 +108,37 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'ac_power': self._ac_power,
|
||||
'backlight_on': self._backlight_on,
|
||||
'battery_low': self._battery_low,
|
||||
'check_zone': self._check_zone,
|
||||
'chime': self._chime,
|
||||
'entry_delay_off': self._entry_delay_off,
|
||||
'programming_mode': self._programming_mode,
|
||||
'ready': self._ready,
|
||||
'zone_bypassed': self._zone_bypassed,
|
||||
}
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
_LOGGER.debug("alarm_disarm: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("alarm_disarm: sending %s1", str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}1".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
_LOGGER.debug("alarm_arm_away: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("alarm_arm_away: sending %s2", str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}2".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
_LOGGER.debug("alarm_arm_home: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("alarm_arm_home: sending %s3", str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}3".format(code))
|
||||
|
||||
def alarm_toggle_chime(self, code=None):
|
||||
"""Send toggle chime command."""
|
||||
if code:
|
||||
self.hass.data[DATA_AD].send("{!s}9".format(code))
|
||||
|
||||
@@ -4,17 +4,18 @@ Interfaces with Alarm.com alarm control panels.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, CONF_CODE,
|
||||
CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.0']
|
||||
|
||||
@@ -44,7 +45,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
"""Represent an Alarm.com status."""
|
||||
"""Representation of an Alarm.com status."""
|
||||
|
||||
def __init__(self, hass, name, code, username, password):
|
||||
"""Initialize the Alarm.com status."""
|
||||
@@ -57,10 +58,8 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
self._password = password
|
||||
self._websession = async_get_clientsession(self._hass)
|
||||
self._state = STATE_UNKNOWN
|
||||
self._alarm = Alarmdotcom(username,
|
||||
password,
|
||||
self._websession,
|
||||
hass.loop)
|
||||
self._alarm = Alarmdotcom(
|
||||
username, password, self._websession, hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_login(self):
|
||||
@@ -80,7 +79,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters if code is defined."""
|
||||
"""Return one or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
@property
|
||||
@@ -116,5 +115,5 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered.')
|
||||
_LOGGER.warning("Wrong code entered")
|
||||
return check
|
||||
|
||||
@@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ARMED = 'armed'
|
||||
|
||||
CONF_HOME_MODE_NAME = 'home_mode_name'
|
||||
CONF_AWAY_MODE_NAME = 'away_mode_name'
|
||||
|
||||
DEPENDENCIES = ['arlo']
|
||||
|
||||
@@ -31,6 +32,7 @@ ICON = 'mdi:security'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
|
||||
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -43,19 +45,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
home_mode_name = config.get(CONF_HOME_MODE_NAME)
|
||||
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
|
||||
base_stations = []
|
||||
for base_station in data.base_stations:
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name))
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name,
|
||||
away_mode_name))
|
||||
async_add_devices(base_stations, True)
|
||||
|
||||
|
||||
class ArloBaseStation(AlarmControlPanel):
|
||||
"""Representation of an Arlo Alarm Control Panel."""
|
||||
|
||||
def __init__(self, data, home_mode_name):
|
||||
def __init__(self, data, home_mode_name, away_mode_name):
|
||||
"""Initialize the alarm control panel."""
|
||||
self._base_station = data
|
||||
self._home_mode_name = home_mode_name
|
||||
self._away_mode_name = away_mode_name
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
@@ -89,8 +94,8 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._base_station.mode = ARMED
|
||||
"""Send arm away command. Uses custom mode."""
|
||||
self._base_station.mode = self._away_mode_name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
@@ -118,4 +123,6 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
return STATE_ALARM_DISARMED
|
||||
elif mode == self._home_mode_name:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode == self._away_mode_name:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return None
|
||||
|
||||
92
homeassistant/components/alarm_control_panel/canary.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Support for Canary alarm.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.canary/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.canary import DATA_CANARY
|
||||
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
|
||||
|
||||
DEPENDENCIES = ['canary']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Canary alarms."""
|
||||
data = hass.data[DATA_CANARY]
|
||||
devices = []
|
||||
|
||||
for location in data.locations:
|
||||
devices.append(CanaryAlarm(data, location.location_id))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class CanaryAlarm(AlarmControlPanel):
|
||||
"""Representation of a Canary alarm control panel."""
|
||||
|
||||
def __init__(self, data, location_id):
|
||||
"""Initialize a Canary security camera."""
|
||||
self._data = data
|
||||
self._location_id = location_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alarm."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
return location.name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
|
||||
LOCATION_MODE_NIGHT
|
||||
|
||||
location = self._data.get_location(self._location_id)
|
||||
|
||||
if location.is_private:
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
mode = location.mode
|
||||
if mode.name == LOCATION_MODE_AWAY:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif mode.name == LOCATION_MODE_HOME:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode.name == LOCATION_MODE_NIGHT:
|
||||
return STATE_ALARM_ARMED_NIGHT
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
return {
|
||||
'private': location.is_private
|
||||
}
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
self._data.set_location_mode(self._location_id, location.mode.name,
|
||||
True)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from canary.api import LOCATION_MODE_HOME
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
from canary.api import LOCATION_MODE_AWAY
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
from canary.api import LOCATION_MODE_NIGHT
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
|
||||
12
homeassistant/components/alarm_control_panel/concord232.py
Executable file → Normal file
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
REQUIREMENTS = ['concord232==0.15']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,9 +29,9 @@ DEFAULT_PORT = 5007
|
||||
SCAN_INTERVAL = timedelta(seconds=1)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([Concord232Alarm(hass, url, name)])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return False
|
||||
return
|
||||
|
||||
|
||||
class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
@@ -107,7 +107,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
newstate = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
if not newstate == self._state:
|
||||
_LOGGER.info("State Change from %s to %s", self._state, newstate)
|
||||
_LOGGER.info("State change from %s to %s", self._state, newstate)
|
||||
self._state = newstate
|
||||
return self._state
|
||||
|
||||
@@ -121,4 +121,4 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('auto')
|
||||
self._alarm.arm('away')
|
||||
|
||||
@@ -4,27 +4,45 @@ Demo platform that has two fake alarm control panels.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import datetime
|
||||
import homeassistant.components.alarm_control_panel.manual as manual
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_TRIGGERED, CONF_PENDING_TIME)
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME,
|
||||
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo alarm control panel platform."""
|
||||
add_devices([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False, {
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
|
||||
STATE_ALARM_ARMED_AWAY: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_HOME: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_NIGHT: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_DISARMED: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_TRIGGERED: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -10,15 +10,15 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.exceptions as exc
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN,
|
||||
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN)
|
||||
import homeassistant.exceptions as exc
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.22']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.26']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,12 +26,15 @@ CONF_REPORT_SERVER_CODES = 'report_server_codes'
|
||||
CONF_REPORT_SERVER_ENABLED = 'report_server_enabled'
|
||||
CONF_REPORT_SERVER_PORT = 'report_server_port'
|
||||
CONF_REPORT_SERVER_CODES_IGNORE = 'ignore'
|
||||
CONF_VERSION = 'version'
|
||||
|
||||
DEFAULT_NAME = 'Egardia'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_REPORT_SERVER_ENABLED = False
|
||||
DEFAULT_REPORT_SERVER_PORT = 52010
|
||||
DEFAULT_VERSION = 'GATE-01'
|
||||
DOMAIN = 'egardia'
|
||||
D_EGARDIASRV = 'egardiaserver'
|
||||
|
||||
NOTIFICATION_ID = 'egardia_notification'
|
||||
NOTIFICATION_TITLE = 'Egardia'
|
||||
@@ -49,6 +52,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_REPORT_SERVER_CODES): vol.All(cv.ensure_list),
|
||||
@@ -62,6 +66,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Egardia platform."""
|
||||
from pythonegardia import egardiadevice
|
||||
from pythonegardia import egardiaserver
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
@@ -71,41 +76,59 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
rs_enabled = config.get(CONF_REPORT_SERVER_ENABLED)
|
||||
rs_port = config.get(CONF_REPORT_SERVER_PORT)
|
||||
rs_codes = config.get(CONF_REPORT_SERVER_CODES)
|
||||
version = config.get(CONF_VERSION)
|
||||
|
||||
try:
|
||||
egardiasystem = egardiadevice.EgardiaDevice(
|
||||
host, port, username, password, '')
|
||||
host, port, username, password, '', version)
|
||||
except requests.exceptions.RequestException:
|
||||
raise exc.PlatformNotReady()
|
||||
except egardiadevice.UnauthorizedError:
|
||||
_LOGGER.error("Unable to authorize. Wrong password or username")
|
||||
return False
|
||||
return
|
||||
|
||||
add_devices([EgardiaAlarm(
|
||||
name, egardiasystem, hass, rs_enabled, rs_port, rs_codes)], True)
|
||||
eg_dev = EgardiaAlarm(
|
||||
name, egardiasystem, rs_enabled, rs_codes)
|
||||
|
||||
if rs_enabled:
|
||||
# Set up the egardia server
|
||||
_LOGGER.info("Setting up EgardiaServer")
|
||||
try:
|
||||
if D_EGARDIASRV not in hass.data:
|
||||
server = egardiaserver.EgardiaServer('', rs_port)
|
||||
bound = server.bind()
|
||||
if not bound:
|
||||
raise IOError(
|
||||
"Binding error occurred while starting EgardiaServer")
|
||||
hass.data[D_EGARDIASRV] = server
|
||||
server.start()
|
||||
except IOError:
|
||||
return
|
||||
hass.data[D_EGARDIASRV].register_callback(eg_dev.handle_status_event)
|
||||
|
||||
def handle_stop_event(event):
|
||||
"""Call function for Home Assistant stop event."""
|
||||
hass.data[D_EGARDIASRV].stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event)
|
||||
|
||||
add_devices([eg_dev], True)
|
||||
|
||||
|
||||
class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a Egardia alarm."""
|
||||
|
||||
def __init__(self, name, egardiasystem, hass, rs_enabled=False,
|
||||
rs_port=None, rs_codes=None):
|
||||
"""Initialize object."""
|
||||
def __init__(self, name, egardiasystem, rs_enabled=False, rs_codes=None):
|
||||
"""Initialize the Egardia alarm."""
|
||||
self._name = name
|
||||
self._egardiasystem = egardiasystem
|
||||
self._status = STATE_UNKNOWN
|
||||
self._status = None
|
||||
self._rs_enabled = rs_enabled
|
||||
self._rs_port = rs_port
|
||||
self._hass = hass
|
||||
|
||||
if rs_codes is not None:
|
||||
self._rs_codes = rs_codes[0]
|
||||
else:
|
||||
self._rs_codes = rs_codes
|
||||
|
||||
if self._rs_enabled:
|
||||
self.listen_to_system_status()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
@@ -116,17 +139,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the state of the device."""
|
||||
return self._status
|
||||
|
||||
def handle_system_status_event(self, event):
|
||||
"""Handle egardia_system_status_event."""
|
||||
if event.data.get('status') is not None:
|
||||
statuscode = event.data.get('status')
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll if no report server is enabled."""
|
||||
if not self._rs_enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
def handle_status_event(self, event):
|
||||
"""Handle the Egardia system status event."""
|
||||
statuscode = event.get('status')
|
||||
if statuscode is not None:
|
||||
status = self.lookupstatusfromcode(statuscode)
|
||||
self.parsestatus(status)
|
||||
|
||||
def listen_to_system_status(self):
|
||||
"""Subscribe to egardia_system_status event."""
|
||||
self._hass.bus.listen(
|
||||
'egardia_system_status', self.handle_system_status_event)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def lookupstatusfromcode(self, statuscode):
|
||||
"""Look at the rs_codes and returns the status from the code."""
|
||||
@@ -161,9 +187,8 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update the alarm status."""
|
||||
if not self._rs_enabled:
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -6,7 +6,6 @@ https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -14,7 +13,6 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.components.envisalink import (
|
||||
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
|
||||
@@ -69,14 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
for device in target_devices:
|
||||
device.async_alarm_keypress(keypress)
|
||||
|
||||
# Register Envisalink specific services
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler,
|
||||
descriptions.get(SERVICE_ALARM_KEYPRESS), schema=ALARM_KEYPRESS_SCHEMA)
|
||||
schema=ALARM_KEYPRESS_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
107
homeassistant/components/alarm_control_panel/ialarm.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Interfaces with iAlarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.ialarm/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyialarm==0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'iAlarm'
|
||||
|
||||
|
||||
def no_application_protocol(value):
|
||||
"""Validate that value is without the application protocol."""
|
||||
protocol_separator = "://"
|
||||
if not value or protocol_separator in value:
|
||||
raise vol.Invalid(
|
||||
'Invalid host, {} is not allowed'.format(protocol_separator))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up an iAlarm control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
url = 'http://{}'.format(host)
|
||||
ialarm = IAlarmPanel(name, username, password, url)
|
||||
add_devices([ialarm], True)
|
||||
|
||||
|
||||
class IAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an iAlarm status."""
|
||||
|
||||
def __init__(self, name, username, password, url):
|
||||
"""Initialize the iAlarm status."""
|
||||
from pyialarm import IAlarm
|
||||
|
||||
self._name = name
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._url = url
|
||||
self._state = None
|
||||
self._client = IAlarm(username, password, url)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
status = self._client.get_status()
|
||||
_LOGGER.debug('iAlarm status: %s', status)
|
||||
if status:
|
||||
status = int(status)
|
||||
|
||||
if status == self._client.DISARMED:
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status == self._client.ARMED_AWAY:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
state = None
|
||||
|
||||
self._state = state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._client.disarm()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._client.arm_away()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._client.arm_stay()
|
||||
@@ -11,28 +11,48 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER)
|
||||
CONF_CODE, CONF_DELAY_TIME, CONF_DISARM_AFTER_TRIGGER, CONF_NAME,
|
||||
CONF_PENDING_TIME, CONF_PLATFORM, CONF_TRIGGER_TIME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
|
||||
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED]
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
"""Validate the state."""
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PRETRIGGER_STATES:
|
||||
if CONF_DELAY_TIME not in config[state]:
|
||||
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
|
||||
if CONF_TRIGGER_TIME not in config[state]:
|
||||
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
@@ -40,30 +60,47 @@ def _state_validator(config):
|
||||
return config
|
||||
|
||||
|
||||
STATE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
def _state_schema(state):
|
||||
"""Validate the state."""
|
||||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All({
|
||||
vol.Required(CONF_PLATFORM): 'manual',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
}, _state_validator))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the manual alarm platform."""
|
||||
@@ -71,8 +108,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
|
||||
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
|
||||
config.get(CONF_CODE_TEMPLATE),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config
|
||||
)])
|
||||
@@ -83,31 +119,41 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for 'trigger_time'. After that will be
|
||||
triggered for 'trigger_time', after that we return to the previous state
|
||||
or disarm if `disarm_after_trigger` is true.
|
||||
When triggered, will be pending for the triggering state's 'delay_time'
|
||||
plus the triggered state's 'pending_time'.
|
||||
After that will be triggered for 'trigger_time', after that we return to
|
||||
the previous state or disarm if `disarm_after_trigger` is true.
|
||||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time, trigger_time,
|
||||
def __init__(self, hass, name, code, code_template,
|
||||
disarm_after_trigger, config):
|
||||
"""Init the manual alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
if code_template:
|
||||
self._code = code_template
|
||||
self._code.hass = hass
|
||||
else:
|
||||
self._code = code or None
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._pre_trigger_state = self._state
|
||||
self._previous_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._pending_time_by_state = {}
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
self._pending_time_by_state[state] = datetime.timedelta(
|
||||
seconds=config[state][CONF_PENDING_TIME])
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the plling state."""
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -118,15 +164,16 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state == STATE_ALARM_TRIGGERED:
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time_by_state[self._state] +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
self._state = self._pre_trigger_state
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
@@ -135,13 +182,28 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
return self._state
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
@property
|
||||
def _active_state(self):
|
||||
"""Get the current state."""
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
else:
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
"""Get the pending time."""
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
return self._state_ts + pending_time > dt_util.utcnow()
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
pending_time += self._delay_time_by_state[self._previous_state]
|
||||
return pending_time
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
"""Get if the action is in the pending time window."""
|
||||
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters."""
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
@@ -174,27 +236,44 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
def alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""
|
||||
Send alarm trigger command.
|
||||
|
||||
No code needed, a trigger time of zero for the current state
|
||||
disables the alarm.
|
||||
"""
|
||||
if not self._trigger_time_by_state[self._active_state]:
|
||||
return
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
"""Update the state."""
|
||||
if self._state == state:
|
||||
return
|
||||
|
||||
self._previous_state = self._state
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
|
||||
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._trigger_time + pending_time)
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
@@ -202,7 +281,14 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if self._code is None:
|
||||
return True
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
@@ -213,6 +299,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
@@ -16,8 +16,8 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER)
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
||||
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
@@ -26,28 +26,47 @@ from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
|
||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
||||
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
||||
DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
|
||||
DEFAULT_DISARM = 'DISARM'
|
||||
|
||||
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED]
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
"""Validate the state."""
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PRETRIGGER_STATES:
|
||||
if CONF_DELAY_TIME not in config[state]:
|
||||
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
|
||||
if CONF_TRIGGER_TIME not in config[state]:
|
||||
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
@@ -55,27 +74,45 @@ def _state_validator(config):
|
||||
return config
|
||||
|
||||
|
||||
STATE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
def _state_schema(state):
|
||||
"""Validate the state."""
|
||||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PLATFORM): 'manual_mqtt',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
@@ -84,8 +121,6 @@ PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||
}), _state_validator))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the manual MQTT alarm platform."""
|
||||
@@ -93,8 +128,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
|
||||
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
|
||||
config.get(CONF_CODE_TEMPLATE),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config.get(mqtt.CONF_STATE_TOPIC),
|
||||
config.get(mqtt.CONF_COMMAND_TOPIC),
|
||||
@@ -111,31 +145,39 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for 'trigger_time'. After that will be
|
||||
triggered for 'trigger_time', after that we return to the previous state
|
||||
or disarm if `disarm_after_trigger` is true.
|
||||
When triggered, will be pending for the triggering state's 'delay_time'
|
||||
plus the triggered state's 'pending_time'.
|
||||
After that will be triggered for 'trigger_time', after that we return to
|
||||
the previous state or disarm if `disarm_after_trigger` is true.
|
||||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time,
|
||||
trigger_time, disarm_after_trigger,
|
||||
state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away,
|
||||
payload_arm_night, config):
|
||||
def __init__(self, hass, name, code, code_template, disarm_after_trigger,
|
||||
state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, payload_arm_night,
|
||||
config):
|
||||
"""Init the manual MQTT alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._pending_time = datetime.timedelta(seconds=pending_time)
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
if code_template:
|
||||
self._code = code_template
|
||||
self._code.hass = hass
|
||||
else:
|
||||
self._code = code or None
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._pre_trigger_state = self._state
|
||||
self._previous_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._pending_time_by_state = {}
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
self._pending_time_by_state[state] = datetime.timedelta(
|
||||
seconds=config[state][CONF_PENDING_TIME])
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
@@ -158,15 +200,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state == STATE_ALARM_TRIGGERED:
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time_by_state[self._state] +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
self._state = self._pre_trigger_state
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
@@ -175,13 +218,28 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
return self._state
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
@property
|
||||
def _active_state(self):
|
||||
"""Get the current state."""
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
else:
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
"""Get the pending time."""
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
return self._state_ts + pending_time > dt_util.utcnow()
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
pending_time += self._delay_time_by_state[self._previous_state]
|
||||
return pending_time
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
"""Get if the action is in the pending time window."""
|
||||
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters."""
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
@@ -215,26 +273,36 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
self._update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
"""
|
||||
Send alarm trigger command.
|
||||
|
||||
No code needed, a trigger time of zero for the current state
|
||||
disables the alarm.
|
||||
"""
|
||||
if not self._trigger_time_by_state[self._active_state]:
|
||||
return
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
"""Update the state."""
|
||||
if self._state == state:
|
||||
return
|
||||
|
||||
self._previous_state = self._state
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
|
||||
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._trigger_time + pending_time)
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
@@ -242,7 +310,14 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if self._code is None:
|
||||
return True
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
@@ -253,12 +328,13 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
"""Subscribe to MQTT events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@@ -287,5 +363,5 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
@asyncio.coroutine
|
||||
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
||||
"""Publish state change to MQTT."""
|
||||
mqtt.async_publish(self.hass, self._state_topic, new_state.state,
|
||||
self._qos, True)
|
||||
mqtt.async_publish(
|
||||
self.hass, self._state_topic, new_state.state, self._qos, True)
|
||||
|
||||
@@ -17,7 +17,9 @@ from homeassistant.const import (
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
||||
CONF_NAME, CONF_CODE)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS)
|
||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -40,7 +42,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||
})
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -54,15 +56,21 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||
config.get(CONF_CODE))])
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE))])
|
||||
|
||||
|
||||
class MqttAlarm(alarm.AlarmControlPanel):
|
||||
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, code):
|
||||
payload_arm_home, payload_arm_away, code, availability_topic,
|
||||
payload_available, payload_not_available):
|
||||
"""Init the MQTT Alarm Control Panel."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
self._state = STATE_UNKNOWN
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
@@ -73,11 +81,11 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._code = code
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
"""Subscribe mqtt events."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Run when new MQTT message has been received."""
|
||||
@@ -89,7 +97,7 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
||||
self._state = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
|
||||
@@ -12,8 +12,8 @@ import voluptuous as vol
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, CONF_NAME, CONF_HOST, CONF_PORT)
|
||||
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.4']
|
||||
@@ -25,14 +25,14 @@ DEFAULT_NAME = 'NX584'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the nx584 platform."""
|
||||
"""Set up the NX584 platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
@@ -88,7 +88,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
self._state = STATE_UNKNOWN
|
||||
zones = []
|
||||
except IndexError:
|
||||
_LOGGER.error("nx584 reports no partitions")
|
||||
_LOGGER.error("NX584 reports no partitions")
|
||||
self._state = STATE_UNKNOWN
|
||||
zones = []
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ import asyncio
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.satel_integra import (CONF_ARM_HOME_MODE,
|
||||
DATA_SATEL,
|
||||
SIGNAL_PANEL_MESSAGE)
|
||||
from homeassistant.components.satel_integra import (
|
||||
CONF_ARM_HOME_MODE, DATA_SATEL, SIGNAL_PANEL_MESSAGE)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
@@ -21,12 +20,12 @@ DEPENDENCIES = ['satel_integra']
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up for AlarmDecoder alarm panels."""
|
||||
"""Set up for Satel Integra alarm panels."""
|
||||
if not discovery_info:
|
||||
return
|
||||
|
||||
device = SatelIntegraAlarmPanel("Alarm Panel",
|
||||
discovery_info.get(CONF_ARM_HOME_MODE))
|
||||
device = SatelIntegraAlarmPanel(
|
||||
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE))
|
||||
async_add_devices([device])
|
||||
|
||||
|
||||
@@ -47,7 +46,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||
|
||||
@callback
|
||||
def _message_callback(self, message):
|
||||
|
||||
"""Handle received messages."""
|
||||
if message != self._state:
|
||||
self._state = message
|
||||
self.async_schedule_update_ha_state()
|
||||
@@ -90,5 +89,5 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
yield from self.hass.data[DATA_SATEL].arm(code,
|
||||
self._arm_home_mode)
|
||||
yield from self.hass.data[DATA_SATEL].arm(
|
||||
code, self._arm_home_mode)
|
||||
|
||||
@@ -59,3 +59,13 @@ envisalink_alarm_keypress:
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel (1-6 characters).'
|
||||
example: '*71'
|
||||
|
||||
alarmdecoder_alarm_toggle_chime:
|
||||
description: Send the alarm the toggle chime command.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: A required code to toggle the alarm control panel chime with.
|
||||
example: 1234
|
||||
|
||||
@@ -11,9 +11,9 @@ import voluptuous as vol
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN, CONF_CODE, CONF_NAME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==1.0.5']
|
||||
@@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'SimpliSafe'
|
||||
DOMAIN = 'simplisafe'
|
||||
|
||||
NOTIFICATION_ID = 'simplisafe_notification'
|
||||
NOTIFICATION_TITLE = 'SimpliSafe Setup'
|
||||
|
||||
@@ -65,7 +66,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation a SimpliSafe alarm."""
|
||||
"""Representation of a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, simplisafe, name, code):
|
||||
"""Initialize the SimpliSafe alarm."""
|
||||
@@ -82,7 +83,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters if code is defined."""
|
||||
"""Return one or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
@property
|
||||
@@ -103,12 +104,12 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'temperature': self.simplisafe.temperature(),
|
||||
'alarm': self.simplisafe.alarm(),
|
||||
'co': self.simplisafe.carbon_monoxide(),
|
||||
'fire': self.simplisafe.fire(),
|
||||
'alarm': self.simplisafe.alarm(),
|
||||
'flood': self.simplisafe.flood(),
|
||||
'last_event': self.simplisafe.last_event(),
|
||||
'flood': self.simplisafe.flood()
|
||||
'temperature': self.simplisafe.temperature(),
|
||||
}
|
||||
|
||||
def update(self):
|
||||
|
||||
@@ -9,26 +9,27 @@ import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.spc import (
|
||||
SpcWebGateway, ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY)
|
||||
ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPC_AREA_MODE_TO_STATE = {'0': STATE_ALARM_DISARMED,
|
||||
'1': STATE_ALARM_ARMED_HOME,
|
||||
'3': STATE_ALARM_ARMED_AWAY}
|
||||
SPC_AREA_MODE_TO_STATE = {
|
||||
'0': STATE_ALARM_DISARMED,
|
||||
'1': STATE_ALARM_ARMED_HOME,
|
||||
'3': STATE_ALARM_ARMED_AWAY,
|
||||
}
|
||||
|
||||
|
||||
def _get_alarm_state(spc_mode):
|
||||
"""Get the alarm state."""
|
||||
return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the SPC alarm control panel platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
||||
@@ -42,7 +43,7 @@ def async_setup_platform(hass, config, async_add_devices,
|
||||
|
||||
|
||||
class SpcAlarm(alarm.AlarmControlPanel):
|
||||
"""Represents the SPC alarm panel."""
|
||||
"""Representation of the SPC alarm panel."""
|
||||
|
||||
def __init__(self, api, area):
|
||||
"""Initialize the SPC alarm panel."""
|
||||
@@ -57,7 +58,7 @@ class SpcAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Calbback for init handlers."""
|
||||
"""Call for adding new entities."""
|
||||
self.hass.data[DATA_REGISTRY].register_alarm_device(
|
||||
self._area_id, self)
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.13']
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.16']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -76,6 +78,8 @@ class TotalConnect(alarm.AlarmControlPanel):
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY_NIGHT:
|
||||
state = STATE_ALARM_ARMED_NIGHT
|
||||
elif status == self._client.ARMED_CUSTOM_BYPASS:
|
||||
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||
elif status == self._client.ARMING:
|
||||
state = STATE_ALARM_ARMING
|
||||
elif status == self._client.DISARMING:
|
||||
|
||||
@@ -8,8 +8,8 @@ import logging
|
||||
from time import sleep
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.components.verisure import (CONF_ALARM, CONF_CODE_DIGITS)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
@@ -43,7 +43,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a Verisure alarm status."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initalize the Verisure alarm panel."""
|
||||
"""Initialize the Verisure alarm panel."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = hub.config.get(CONF_CODE_DIGITS)
|
||||
self._changed_by = None
|
||||
|
||||
@@ -8,11 +8,10 @@ import asyncio
|
||||
import logging
|
||||
|
||||
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)
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
from homeassistant.components.wink import DOMAIN, WinkDevice
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,7 +40,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
"""Call when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
|
||||
|
||||
@property
|
||||
|
||||
@@ -4,18 +4,18 @@ Support for AlarmDecoder devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
||||
|
||||
REQUIREMENTS = ['alarmdecoder==0.12.3']
|
||||
REQUIREMENTS = ['alarmdecoder==1.13.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,6 +32,7 @@ CONF_DEVICE_TYPE = 'type'
|
||||
CONF_PANEL_DISPLAY = 'panel_display'
|
||||
CONF_ZONE_NAME = 'name'
|
||||
CONF_ZONE_TYPE = 'type'
|
||||
CONF_ZONE_RFID = 'rfid'
|
||||
CONF_ZONES = 'zones'
|
||||
|
||||
DEFAULT_DEVICE_TYPE = 'socket'
|
||||
@@ -51,6 +52,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
|
||||
|
||||
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
||||
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
||||
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
|
||||
|
||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
||||
@@ -67,13 +69,15 @@ DEVICE_USB_SCHEMA = vol.Schema({
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONE_NAME): cv.string,
|
||||
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string})
|
||||
vol.Optional(CONF_ZONE_TYPE,
|
||||
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
|
||||
vol.Optional(CONF_ZONE_RFID): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA,
|
||||
DEVICE_SERIAL_SCHEMA,
|
||||
DEVICE_USB_SCHEMA),
|
||||
vol.Required(CONF_DEVICE): vol.Any(
|
||||
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA,
|
||||
DEVICE_USB_SCHEMA),
|
||||
vol.Optional(CONF_PANEL_DISPLAY,
|
||||
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
@@ -81,14 +85,14 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
def setup(hass, config):
|
||||
"""Set up for the AlarmDecoder devices."""
|
||||
from alarmdecoder import AlarmDecoder
|
||||
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
|
||||
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
restart = False
|
||||
device = conf.get(CONF_DEVICE)
|
||||
display = conf.get(CONF_PANEL_DISPLAY)
|
||||
zones = conf.get(CONF_ZONES)
|
||||
@@ -99,32 +103,55 @@ def async_setup(hass, config):
|
||||
path = DEFAULT_DEVICE_PATH
|
||||
baud = DEFAULT_DEVICE_BAUD
|
||||
|
||||
sync_connect = asyncio.Future(loop=hass.loop)
|
||||
|
||||
def handle_open(device):
|
||||
"""Handle the successful connection."""
|
||||
_LOGGER.info("Established a connection with the alarmdecoder")
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
||||
sync_connect.set_result(True)
|
||||
|
||||
@callback
|
||||
def stop_alarmdecoder(event):
|
||||
"""Handle the shutdown of AlarmDecoder."""
|
||||
_LOGGER.debug("Shutting down alarmdecoder")
|
||||
nonlocal restart
|
||||
restart = False
|
||||
controller.close()
|
||||
|
||||
@callback
|
||||
def open_connection(now=None):
|
||||
"""Open a connection to AlarmDecoder."""
|
||||
from alarmdecoder.util import NoDeviceError
|
||||
nonlocal restart
|
||||
try:
|
||||
controller.open(baud)
|
||||
except NoDeviceError:
|
||||
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
|
||||
hass.helpers.event.track_point_in_time(
|
||||
open_connection, dt_util.utcnow() + timedelta(seconds=5))
|
||||
return
|
||||
_LOGGER.debug("Established a connection with the alarmdecoder")
|
||||
restart = True
|
||||
|
||||
def handle_closed_connection(event):
|
||||
"""Restart after unexpected loss of connection."""
|
||||
nonlocal restart
|
||||
if not restart:
|
||||
return
|
||||
restart = False
|
||||
_LOGGER.warning("AlarmDecoder unexpectedly lost connection.")
|
||||
hass.add_job(open_connection)
|
||||
|
||||
def handle_message(sender, message):
|
||||
"""Handle message from AlarmDecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message)
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_PANEL_MESSAGE, message)
|
||||
|
||||
def handle_rfx_message(sender, message):
|
||||
"""Handle RFX message from AlarmDecoder."""
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_RFX_MESSAGE, message)
|
||||
|
||||
def zone_fault_callback(sender, zone):
|
||||
"""Handle zone fault from AlarmDecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone)
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_FAULT, zone)
|
||||
|
||||
def zone_restore_callback(sender, zone):
|
||||
"""Handle zone restore from AlarmDecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone)
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_RESTORE, zone)
|
||||
|
||||
controller = False
|
||||
if device_type == 'socket':
|
||||
@@ -139,30 +166,25 @@ def async_setup(hass, config):
|
||||
AlarmDecoder(USBDevice.find())
|
||||
return False
|
||||
|
||||
controller.on_open += handle_open
|
||||
controller.on_message += handle_message
|
||||
controller.on_rfx_message += handle_rfx_message
|
||||
controller.on_zone_fault += zone_fault_callback
|
||||
controller.on_zone_restore += zone_restore_callback
|
||||
controller.on_close += handle_closed_connection
|
||||
|
||||
hass.data[DATA_AD] = controller
|
||||
|
||||
controller.open(baud)
|
||||
open_connection()
|
||||
|
||||
result = yield from sync_connect
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
hass.async_add_job(
|
||||
async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf,
|
||||
config))
|
||||
load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
|
||||
|
||||
if zones:
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config))
|
||||
load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)
|
||||
|
||||
if display:
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'sensor', DOMAIN, conf, config))
|
||||
load_platform(hass, 'sensor', DOMAIN, conf, config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -7,12 +7,10 @@ https://home-assistant.io/components/alert/
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
@@ -129,22 +127,16 @@ def async_setup(hass, config):
|
||||
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
|
||||
all_alerts[entity.entity_id] = entity
|
||||
|
||||
# Read descriptions
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = descriptions.get(DOMAIN, {})
|
||||
|
||||
# Setup service calls
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA)
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TURN_ON), schema=ALERT_SERVICE_SCHEMA)
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TOGGLE), schema=ALERT_SERVICE_SCHEMA)
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
|
||||
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
|
||||
if tasks:
|
||||
|
||||
@@ -10,18 +10,33 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import entityfilter
|
||||
|
||||
from . import flash_briefings, intent, smart_home
|
||||
from .const import (
|
||||
DOMAIN, CONF_UID, CONF_TITLE, CONF_AUDIO, CONF_TEXT, CONF_DISPLAY_URL)
|
||||
from . import flash_briefings, intent
|
||||
CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN,
|
||||
CONF_FILTER, CONF_ENTITY_CONFIG)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
CONF_SMART_HOME = 'smart_home'
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(smart_home.CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string,
|
||||
vol.Optional(smart_home.CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
SMART_HOME_SCHEMA = vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_FILTER,
|
||||
default=lambda: entityfilter.generate_filter([], [], [], [])
|
||||
): entityfilter.FILTER_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
@@ -33,7 +48,10 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TEXT, default=""): cv.template,
|
||||
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
||||
}]),
|
||||
}
|
||||
},
|
||||
# vol.Optional here would mean we couldn't distinguish between an empty
|
||||
# smart_home: and none at all.
|
||||
CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None),
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -49,4 +67,12 @@ def async_setup(hass, config):
|
||||
if flash_briefings_config:
|
||||
flash_briefings.async_setup(hass, flash_briefings_config)
|
||||
|
||||
try:
|
||||
smart_home_config = config[CONF_SMART_HOME]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
smart_home_config = smart_home_config or SMART_HOME_SCHEMA({})
|
||||
smart_home.async_setup(hass, smart_home_config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -8,6 +8,9 @@ CONF_AUDIO = 'audio'
|
||||
CONF_TEXT = 'text'
|
||||
CONF_DISPLAY_URL = 'display_url'
|
||||
|
||||
CONF_FILTER = 'filter'
|
||||
CONF_ENTITY_CONFIG = 'entity_config'
|
||||
|
||||
ATTR_UID = 'uid'
|
||||
ATTR_UPDATE_DATE = 'updateDate'
|
||||
ATTR_TITLE_TEXT = 'titleText'
|
||||
|
||||
@@ -5,19 +5,18 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
"""
|
||||
import copy
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from homeassistant.components import http
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components import http
|
||||
|
||||
from .const import (
|
||||
CONF_UID, CONF_TITLE, CONF_AUDIO, CONF_TEXT, CONF_DISPLAY_URL, ATTR_UID,
|
||||
ATTR_UPDATE_DATE, ATTR_TITLE_TEXT, ATTR_STREAM_URL, ATTR_MAIN_TEXT,
|
||||
ATTR_REDIRECTION_URL, DATE_FORMAT)
|
||||
|
||||
ATTR_MAIN_TEXT, ATTR_REDIRECTION_URL, ATTR_STREAM_URL, ATTR_TITLE_TEXT,
|
||||
ATTR_UID, ATTR_UPDATE_DATE, CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT,
|
||||
CONF_TITLE, CONF_UID, DATE_FORMAT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,11 +45,11 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
@callback
|
||||
def get(self, request, briefing_id):
|
||||
"""Handle Alexa Flash Briefing request."""
|
||||
_LOGGER.debug('Received Alexa flash briefing request for: %s',
|
||||
_LOGGER.debug("Received Alexa flash briefing request for: %s",
|
||||
briefing_id)
|
||||
|
||||
if self.flash_briefings.get(briefing_id) is None:
|
||||
err = 'No configured Alexa flash briefing was found for: %s'
|
||||
err = "No configured Alexa flash briefing was found for: %s"
|
||||
_LOGGER.error(err, briefing_id)
|
||||
return b'', 404
|
||||
|
||||
|
||||
@@ -3,29 +3,31 @@ Support for Alexa skill service end point.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import enum
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.components import http
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from .const import DOMAIN, SYN_RESOLUTION_MATCH
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HANDLERS = Registry()
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
"""The Alexa speech types."""
|
||||
|
||||
plaintext = "PlainText"
|
||||
ssml = "SSML"
|
||||
plaintext = 'PlainText'
|
||||
ssml = 'SSML'
|
||||
|
||||
|
||||
SPEECH_MAPPINGS = {
|
||||
@@ -37,8 +39,8 @@ SPEECH_MAPPINGS = {
|
||||
class CardType(enum.Enum):
|
||||
"""The Alexa card types."""
|
||||
|
||||
simple = "Simple"
|
||||
link_account = "LinkAccount"
|
||||
simple = 'Simple'
|
||||
link_account = 'LinkAccount'
|
||||
|
||||
|
||||
@callback
|
||||
@@ -47,6 +49,10 @@ def async_setup(hass):
|
||||
hass.http.register_view(AlexaIntentsView)
|
||||
|
||||
|
||||
class UnknownRequest(HomeAssistantError):
|
||||
"""When an unknown Alexa request is passed in."""
|
||||
|
||||
|
||||
class AlexaIntentsView(http.HomeAssistantView):
|
||||
"""Handle Alexa requests."""
|
||||
|
||||
@@ -57,71 +63,114 @@ class AlexaIntentsView(http.HomeAssistantView):
|
||||
def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
hass = request.app['hass']
|
||||
data = yield from request.json()
|
||||
message = yield from request.json()
|
||||
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
|
||||
req = data.get('request')
|
||||
|
||||
if req is None:
|
||||
_LOGGER.error('Received invalid data from Alexa: %s', data)
|
||||
return self.json_message('Expected request value not received',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
req_type = req['type']
|
||||
|
||||
if req_type == 'SessionEndedRequest':
|
||||
return None
|
||||
|
||||
alexa_intent_info = req.get('intent')
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
|
||||
if req_type != 'IntentRequest' and req_type != 'LaunchRequest':
|
||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
||||
return self.json_message(
|
||||
'Received unsupported request: {}'.format(req_type),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
if req_type == 'LaunchRequest':
|
||||
intent_name = data.get('session', {}) \
|
||||
.get('application', {}) \
|
||||
.get('applicationId')
|
||||
else:
|
||||
intent_name = alexa_intent_info['name']
|
||||
_LOGGER.debug("Received Alexa request: %s", message)
|
||||
|
||||
try:
|
||||
intent_response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_name,
|
||||
{key: {'value': value} for key, value
|
||||
in alexa_response.variables.items()})
|
||||
response = yield from async_handle_message(hass, message)
|
||||
return b'' if response is None else self.json(response)
|
||||
except UnknownRequest as err:
|
||||
_LOGGER.warning(str(err))
|
||||
return self.json(intent_error_response(
|
||||
hass, message, str(err)))
|
||||
|
||||
except intent.UnknownIntent as err:
|
||||
_LOGGER.warning('Received unknown intent %s', intent_name)
|
||||
alexa_response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"This intent is not yet configured within Home Assistant.")
|
||||
return self.json(alexa_response)
|
||||
_LOGGER.warning(str(err))
|
||||
return self.json(intent_error_response(
|
||||
hass, message,
|
||||
"This intent is not yet configured within Home Assistant."))
|
||||
|
||||
except intent.InvalidSlotInfo as err:
|
||||
_LOGGER.error('Received invalid slot data from Alexa: %s', err)
|
||||
return self.json_message('Invalid slot data received',
|
||||
HTTP_BAD_REQUEST)
|
||||
except intent.IntentError:
|
||||
_LOGGER.exception('Error handling request for %s', intent_name)
|
||||
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
|
||||
_LOGGER.error("Received invalid slot data from Alexa: %s", err)
|
||||
return self.json(intent_error_response(
|
||||
hass, message,
|
||||
"Invalid slot information received for this intent."))
|
||||
|
||||
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
||||
if intent_speech in intent_response.speech:
|
||||
alexa_response.add_speech(
|
||||
alexa_speech,
|
||||
intent_response.speech[intent_speech]['speech'])
|
||||
break
|
||||
except intent.IntentError as err:
|
||||
_LOGGER.exception(str(err))
|
||||
return self.json(intent_error_response(
|
||||
hass, message, "Error handling intent."))
|
||||
|
||||
if 'simple' in intent_response.card:
|
||||
alexa_response.add_card(
|
||||
CardType.simple, intent_response.card['simple']['title'],
|
||||
intent_response.card['simple']['content'])
|
||||
|
||||
return self.json(alexa_response)
|
||||
def intent_error_response(hass, message, error):
|
||||
"""Return an Alexa response that will speak the error message."""
|
||||
alexa_intent_info = message.get('request').get('intent')
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
alexa_response.add_speech(SpeechType.plaintext, error)
|
||||
return alexa_response.as_dict()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, message):
|
||||
"""Handle an Alexa intent.
|
||||
|
||||
Raises:
|
||||
- UnknownRequest
|
||||
- intent.UnknownIntent
|
||||
- intent.InvalidSlotInfo
|
||||
- intent.IntentError
|
||||
|
||||
"""
|
||||
req = message.get('request')
|
||||
req_type = req['type']
|
||||
|
||||
handler = HANDLERS.get(req_type)
|
||||
|
||||
if not handler:
|
||||
raise UnknownRequest('Received unknown request {}'.format(req_type))
|
||||
|
||||
return (yield from handler(hass, message))
|
||||
|
||||
|
||||
@HANDLERS.register('SessionEndedRequest')
|
||||
@asyncio.coroutine
|
||||
def async_handle_session_end(hass, message):
|
||||
"""Handle a session end request."""
|
||||
return None
|
||||
|
||||
|
||||
@HANDLERS.register('IntentRequest')
|
||||
@HANDLERS.register('LaunchRequest')
|
||||
@asyncio.coroutine
|
||||
def async_handle_intent(hass, message):
|
||||
"""Handle an intent request.
|
||||
|
||||
Raises:
|
||||
- intent.UnknownIntent
|
||||
- intent.InvalidSlotInfo
|
||||
- intent.IntentError
|
||||
|
||||
"""
|
||||
req = message.get('request')
|
||||
alexa_intent_info = req.get('intent')
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
|
||||
if req['type'] == 'LaunchRequest':
|
||||
intent_name = message.get('session', {}) \
|
||||
.get('application', {}) \
|
||||
.get('applicationId')
|
||||
else:
|
||||
intent_name = alexa_intent_info['name']
|
||||
|
||||
intent_response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_name,
|
||||
{key: {'value': value} for key, value
|
||||
in alexa_response.variables.items()})
|
||||
|
||||
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
||||
if intent_speech in intent_response.speech:
|
||||
alexa_response.add_speech(
|
||||
alexa_speech,
|
||||
intent_response.speech[intent_speech]['speech'])
|
||||
break
|
||||
|
||||
if 'simple' in intent_response.card:
|
||||
alexa_response.add_card(
|
||||
CardType.simple, intent_response.card['simple']['title'],
|
||||
intent_response.card['simple']['content'])
|
||||
|
||||
return alexa_response.as_dict()
|
||||
|
||||
|
||||
def resolve_slot_synonyms(key, request):
|
||||
|
||||
@@ -1,79 +1,428 @@
|
||||
"""Support for alexa Smart Home Skill API."""
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from homeassistant.components import (
|
||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
||||
media_player, scene, script, switch, http, sensor)
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_LOCK,
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET)
|
||||
from homeassistant.components import (
|
||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
||||
media_player, scene, script, switch)
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||
CONF_UNIT_OF_MEASUREMENT)
|
||||
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
||||
|
||||
HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_DIRECTIVE = 'directive'
|
||||
API_ENDPOINT = 'endpoint'
|
||||
API_EVENT = 'event'
|
||||
API_CONTEXT = 'context'
|
||||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
|
||||
ATTR_ALEXA_DESCRIPTION = 'alexa_description'
|
||||
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories'
|
||||
ATTR_ALEXA_HIDDEN = 'alexa_hidden'
|
||||
ATTR_ALEXA_NAME = 'alexa_name'
|
||||
API_TEMP_UNITS = {
|
||||
TEMP_FAHRENHEIT: 'FAHRENHEIT',
|
||||
TEMP_CELSIUS: 'CELSIUS',
|
||||
}
|
||||
|
||||
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
||||
|
||||
CONF_DESCRIPTION = 'description'
|
||||
CONF_DISPLAY_CATEGORIES = 'display_categories'
|
||||
|
||||
HANDLERS = Registry()
|
||||
|
||||
|
||||
MAPPING_COMPONENT = {
|
||||
alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
cover.DOMAIN: [
|
||||
'DOOR', ('Alexa.PowerController',), {
|
||||
cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController',
|
||||
}
|
||||
],
|
||||
fan.DOMAIN: [
|
||||
'OTHER', ('Alexa.PowerController',), {
|
||||
fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController',
|
||||
}
|
||||
],
|
||||
group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
light.DOMAIN: [
|
||||
'LIGHT', ('Alexa.PowerController',), {
|
||||
light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController',
|
||||
light.SUPPORT_RGB_COLOR: 'Alexa.ColorController',
|
||||
light.SUPPORT_XY_COLOR: 'Alexa.ColorController',
|
||||
light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController',
|
||||
}
|
||||
],
|
||||
lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None],
|
||||
media_player.DOMAIN: [
|
||||
'TV', ('Alexa.PowerController',), {
|
||||
media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker',
|
||||
media_player.SUPPORT_PLAY: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_STOP: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController',
|
||||
}
|
||||
],
|
||||
scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None],
|
||||
script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
|
||||
class _DisplayCategory(object):
|
||||
"""Possible display categories for Discovery response.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
|
||||
"""
|
||||
|
||||
# Describes a combination of devices set to a specific state, when the
|
||||
# state change must occur in a specific order. For example, a "watch
|
||||
# Neflix" scene might require the: 1. TV to be powered on & 2. Input set to
|
||||
# HDMI1. Applies to Scenes
|
||||
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
|
||||
|
||||
# Indicates media devices with video or photo capabilities.
|
||||
CAMERA = "CAMERA"
|
||||
|
||||
# Indicates a door.
|
||||
DOOR = "DOOR"
|
||||
|
||||
# Indicates light sources or fixtures.
|
||||
LIGHT = "LIGHT"
|
||||
|
||||
# An endpoint that cannot be described in on of the other categories.
|
||||
OTHER = "OTHER"
|
||||
|
||||
# Describes a combination of devices set to a specific state, when the
|
||||
# order of the state change is not important. For example a bedtime scene
|
||||
# might include turning off lights and lowering the thermostat, but the
|
||||
# order is unimportant. Applies to Scenes
|
||||
SCENE_TRIGGER = "SCENE_TRIGGER"
|
||||
|
||||
# Indicates an endpoint that locks.
|
||||
SMARTLOCK = "SMARTLOCK"
|
||||
|
||||
# Indicates modules that are plugged into an existing electrical outlet.
|
||||
# Can control a variety of devices.
|
||||
SMARTPLUG = "SMARTPLUG"
|
||||
|
||||
# Indicates the endpoint is a speaker or speaker system.
|
||||
SPEAKER = "SPEAKER"
|
||||
|
||||
# Indicates in-wall switches wired to the electrical system. Can control a
|
||||
# variety of devices.
|
||||
SWITCH = "SWITCH"
|
||||
|
||||
# Indicates endpoints that report the temperature only.
|
||||
TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR"
|
||||
|
||||
# Indicates endpoints that control temperature, stand-alone air
|
||||
# conditioners, or heaters with direct temperature control.
|
||||
THERMOSTAT = "THERMOSTAT"
|
||||
|
||||
# Indicates the endpoint is a television.
|
||||
# pylint: disable=invalid-name
|
||||
TV = "TV"
|
||||
|
||||
|
||||
def _capability(interface,
|
||||
version=3,
|
||||
supports_deactivation=None,
|
||||
retrievable=None,
|
||||
properties_supported=None,
|
||||
cap_type='AlexaInterface'):
|
||||
"""Return a Smart Home API capability object.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#capability-object
|
||||
|
||||
There are some additional fields allowed but not implemented here since
|
||||
we've no use case for them yet:
|
||||
|
||||
- proactively_reported
|
||||
|
||||
`supports_deactivation` applies only to scenes.
|
||||
"""
|
||||
result = {
|
||||
'type': cap_type,
|
||||
'interface': interface,
|
||||
'version': version,
|
||||
}
|
||||
|
||||
if supports_deactivation is not None:
|
||||
result['supportsDeactivation'] = supports_deactivation
|
||||
|
||||
if retrievable is not None:
|
||||
result['retrievable'] = retrievable
|
||||
|
||||
if properties_supported is not None:
|
||||
result['properties'] = {'supported': properties_supported}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class _EntityCapabilities(object):
|
||||
def __init__(self, config, entity):
|
||||
self.config = config
|
||||
self.entity = entity
|
||||
|
||||
def display_categories(self):
|
||||
"""Return a list of display categories."""
|
||||
entity_conf = self.config.entity_config.get(self.entity.entity_id, {})
|
||||
if CONF_DISPLAY_CATEGORIES in entity_conf:
|
||||
return [entity_conf[CONF_DISPLAY_CATEGORIES]]
|
||||
return self.default_display_categories()
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return a list of default display categories.
|
||||
|
||||
This can be overridden by the user in the Home Assistant configuration.
|
||||
|
||||
See also _DisplayCategory.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def capabilities(self):
|
||||
"""Return a list of supported capabilities.
|
||||
|
||||
If the returned list is empty, the entity will not be discovered.
|
||||
|
||||
You might find _capability() useful.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _GenericCapabilities(_EntityCapabilities):
|
||||
"""A generic, on/off device.
|
||||
|
||||
The choice of last resort.
|
||||
"""
|
||||
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.OTHER]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.PowerController')]
|
||||
|
||||
|
||||
class _SwitchCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.SWITCH]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.PowerController')]
|
||||
|
||||
|
||||
class _CoverCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.DOOR]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = [_capability('Alexa.PowerController')]
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & cover.SUPPORT_SET_POSITION:
|
||||
capabilities.append(_capability('Alexa.PercentageController'))
|
||||
return capabilities
|
||||
|
||||
|
||||
class _LightCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.LIGHT]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = [_capability('Alexa.PowerController')]
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & light.SUPPORT_BRIGHTNESS:
|
||||
capabilities.append(_capability('Alexa.BrightnessController'))
|
||||
if supported & light.SUPPORT_RGB_COLOR:
|
||||
capabilities.append(_capability('Alexa.ColorController'))
|
||||
if supported & light.SUPPORT_XY_COLOR:
|
||||
capabilities.append(_capability('Alexa.ColorController'))
|
||||
if supported & light.SUPPORT_COLOR_TEMP:
|
||||
capabilities.append(
|
||||
_capability('Alexa.ColorTemperatureController'))
|
||||
return capabilities
|
||||
|
||||
|
||||
class _FanCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.OTHER]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = [_capability('Alexa.PowerController')]
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & fan.SUPPORT_SET_SPEED:
|
||||
capabilities.append(_capability('Alexa.PercentageController'))
|
||||
return capabilities
|
||||
|
||||
|
||||
class _LockCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.SMARTLOCK]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.LockController')]
|
||||
|
||||
|
||||
class _MediaPlayerCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.TV]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = [_capability('Alexa.PowerController')]
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & media_player.SUPPORT_VOLUME_SET:
|
||||
capabilities.append(_capability('Alexa.Speaker'))
|
||||
|
||||
playback_features = (media_player.SUPPORT_PLAY |
|
||||
media_player.SUPPORT_PAUSE |
|
||||
media_player.SUPPORT_STOP |
|
||||
media_player.SUPPORT_NEXT_TRACK |
|
||||
media_player.SUPPORT_PREVIOUS_TRACK)
|
||||
if supported & playback_features:
|
||||
capabilities.append(_capability('Alexa.PlaybackController'))
|
||||
|
||||
return capabilities
|
||||
|
||||
|
||||
class _SceneCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.SCENE_TRIGGER]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.SceneController')]
|
||||
|
||||
|
||||
class _ScriptCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.ACTIVITY_TRIGGER]
|
||||
|
||||
def capabilities(self):
|
||||
can_cancel = bool(self.entity.attributes.get('can_cancel'))
|
||||
return [_capability('Alexa.SceneController',
|
||||
supports_deactivation=can_cancel)]
|
||||
|
||||
|
||||
class _GroupCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.SCENE_TRIGGER]
|
||||
|
||||
def capabilities(self):
|
||||
return [_capability('Alexa.SceneController',
|
||||
supports_deactivation=True)]
|
||||
|
||||
|
||||
class _SensorCapabilities(_EntityCapabilities):
|
||||
def default_display_categories(self):
|
||||
# although there are other kinds of sensors, all but temperature
|
||||
# sensors are currently ignored.
|
||||
return [_DisplayCategory.TEMPERATURE_SENSOR]
|
||||
|
||||
def capabilities(self):
|
||||
capabilities = []
|
||||
|
||||
attrs = self.entity.attributes
|
||||
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
):
|
||||
capabilities.append(_capability(
|
||||
'Alexa.TemperatureSensor',
|
||||
retrievable=True,
|
||||
properties_supported=[{'name': 'temperature'}]))
|
||||
|
||||
return capabilities
|
||||
|
||||
|
||||
class _UnknownEntityDomainError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _capabilities_for_entity(config, entity):
|
||||
"""Return an _EntityCapabilities appropriate for given entity.
|
||||
|
||||
raises _UnknownEntityDomainError if the given domain is unsupported.
|
||||
"""
|
||||
if entity.domain not in _CAPABILITIES_FOR_DOMAIN:
|
||||
raise _UnknownEntityDomainError()
|
||||
return _CAPABILITIES_FOR_DOMAIN[entity.domain](config, entity)
|
||||
|
||||
|
||||
_CAPABILITIES_FOR_DOMAIN = {
|
||||
alert.DOMAIN: _GenericCapabilities,
|
||||
automation.DOMAIN: _GenericCapabilities,
|
||||
cover.DOMAIN: _CoverCapabilities,
|
||||
fan.DOMAIN: _FanCapabilities,
|
||||
group.DOMAIN: _GroupCapabilities,
|
||||
input_boolean.DOMAIN: _GenericCapabilities,
|
||||
light.DOMAIN: _LightCapabilities,
|
||||
lock.DOMAIN: _LockCapabilities,
|
||||
media_player.DOMAIN: _MediaPlayerCapabilities,
|
||||
scene.DOMAIN: _SceneCapabilities,
|
||||
script.DOMAIN: _ScriptCapabilities,
|
||||
switch.DOMAIN: _SwitchCapabilities,
|
||||
sensor.DOMAIN: _SensorCapabilities,
|
||||
}
|
||||
|
||||
|
||||
Config = namedtuple('AlexaConfig', 'filter')
|
||||
class _Cause(object):
|
||||
"""Possible causes for property changes.
|
||||
|
||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object
|
||||
"""
|
||||
|
||||
# Indicates that the event was caused by a customer interaction with an
|
||||
# application. For example, a customer switches on a light, or locks a door
|
||||
# using the Alexa app or an app provided by a device vendor.
|
||||
APP_INTERACTION = 'APP_INTERACTION'
|
||||
|
||||
# Indicates that the event was caused by a physical interaction with an
|
||||
# endpoint. For example manually switching on a light or manually locking a
|
||||
# door lock
|
||||
PHYSICAL_INTERACTION = 'PHYSICAL_INTERACTION'
|
||||
|
||||
# Indicates that the event was caused by the periodic poll of an appliance,
|
||||
# which found a change in value. For example, you might poll a temperature
|
||||
# sensor every hour, and send the updated temperature to Alexa.
|
||||
PERIODIC_POLL = 'PERIODIC_POLL'
|
||||
|
||||
# Indicates that the event was caused by the application of a device rule.
|
||||
# For example, a customer configures a rule to switch on a light if a
|
||||
# motion sensor detects motion. In this case, Alexa receives an event from
|
||||
# the motion sensor, and another event from the light to indicate that its
|
||||
# state change was caused by the rule.
|
||||
RULE_TRIGGER = 'RULE_TRIGGER'
|
||||
|
||||
# Indicates that the event was caused by a voice interaction with Alexa.
|
||||
# For example a user speaking to their Echo device.
|
||||
VOICE_INTERACTION = 'VOICE_INTERACTION'
|
||||
|
||||
|
||||
class Config:
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
def __init__(self, should_expose, entity_config=None):
|
||||
"""Initialize the configuration."""
|
||||
self.should_expose = should_expose
|
||||
self.entity_config = entity_config or {}
|
||||
|
||||
|
||||
@ha.callback
|
||||
def async_setup(hass, config):
|
||||
"""Activate Smart Home functionality of Alexa component.
|
||||
|
||||
This is optional, triggered by having a `smart_home:` sub-section in the
|
||||
alexa configuration.
|
||||
|
||||
Even if that's disabled, the functionality in this module may still be used
|
||||
by the cloud component which will call async_handle_message directly.
|
||||
"""
|
||||
smart_home_config = Config(
|
||||
should_expose=config[CONF_FILTER],
|
||||
entity_config=config.get(CONF_ENTITY_CONFIG),
|
||||
)
|
||||
hass.http.register_view(SmartHomeView(smart_home_config))
|
||||
|
||||
|
||||
class SmartHomeView(http.HomeAssistantView):
|
||||
"""Expose Smart Home v3 payload interface via HTTP POST."""
|
||||
|
||||
url = SMART_HOME_HTTP_ENDPOINT
|
||||
name = 'api:alexa:smart_home'
|
||||
|
||||
def __init__(self, smart_home_config):
|
||||
"""Initialize."""
|
||||
self.smart_home_config = smart_home_config
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle Alexa Smart Home requests.
|
||||
|
||||
The Smart Home API requires the endpoint to be implemented in AWS
|
||||
Lambda, which will need to forward the requests to here and pass back
|
||||
the response.
|
||||
"""
|
||||
hass = request.app['hass']
|
||||
message = yield from request.json()
|
||||
|
||||
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
|
||||
|
||||
response = yield from async_handle_message(
|
||||
hass, self.smart_home_config, message)
|
||||
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
|
||||
return b'' if response is None else self.json(response)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -96,7 +445,11 @@ def async_handle_message(hass, config, message):
|
||||
return (yield from funct_ref(hass, config, message))
|
||||
|
||||
|
||||
def api_message(request, name='Response', namespace='Alexa', payload=None):
|
||||
def api_message(request,
|
||||
name='Response',
|
||||
namespace='Alexa',
|
||||
payload=None,
|
||||
context=None):
|
||||
"""Create a API formatted response message.
|
||||
|
||||
Async friendly.
|
||||
@@ -124,6 +477,9 @@ def api_message(request, name='Response', namespace='Alexa', payload=None):
|
||||
if API_ENDPOINT in request:
|
||||
response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy()
|
||||
|
||||
if context is not None:
|
||||
response[API_CONTEXT] = context
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -150,64 +506,41 @@ def async_api_discovery(hass, config, request):
|
||||
discovery_endpoints = []
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
if not config.filter(entity.entity_id):
|
||||
if not config.should_expose(entity.entity_id):
|
||||
_LOGGER.debug("Not exposing %s because filtered by config",
|
||||
entity.entity_id)
|
||||
continue
|
||||
|
||||
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
|
||||
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
|
||||
entity.entity_id)
|
||||
try:
|
||||
entity_capabilities = _capabilities_for_entity(config, entity)
|
||||
except _UnknownEntityDomainError:
|
||||
continue
|
||||
|
||||
class_data = MAPPING_COMPONENT.get(entity.domain)
|
||||
entity_conf = config.entity_config.get(entity.entity_id, {})
|
||||
|
||||
if not class_data:
|
||||
continue
|
||||
|
||||
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name)
|
||||
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
|
||||
entity.entity_id)
|
||||
friendly_name = entity_conf.get(CONF_NAME, entity.name)
|
||||
description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id)
|
||||
|
||||
# Required description as per Amazon Scene docs
|
||||
if entity.domain == scene.DOMAIN:
|
||||
scene_fmt = '%s (Scene connected via Home Assistant)'
|
||||
scene_fmt = '{} (Scene connected via Home Assistant)'
|
||||
description = scene_fmt.format(description)
|
||||
|
||||
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES
|
||||
display_categories = entity.attributes.get(cat_key, class_data[0])
|
||||
|
||||
endpoint = {
|
||||
'displayCategories': [display_categories],
|
||||
'displayCategories': entity_capabilities.display_categories(),
|
||||
'additionalApplianceDetails': {},
|
||||
'endpointId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyName': friendly_name,
|
||||
'description': description,
|
||||
'manufacturerName': 'Home Assistant',
|
||||
}
|
||||
actions = set()
|
||||
|
||||
# static actions
|
||||
if class_data[1]:
|
||||
actions |= set(class_data[1])
|
||||
|
||||
# dynamic actions
|
||||
if class_data[2]:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
for feature, action_name in class_data[2].items():
|
||||
if feature & supported > 0:
|
||||
actions.add(action_name)
|
||||
|
||||
# Write action into capabilities
|
||||
capabilities = []
|
||||
for action in actions:
|
||||
capabilities.append({
|
||||
'type': 'AlexaInterface',
|
||||
'interface': action,
|
||||
'version': 3,
|
||||
})
|
||||
|
||||
endpoint['capabilities'] = capabilities
|
||||
alexa_capabilities = entity_capabilities.capabilities()
|
||||
if not alexa_capabilities:
|
||||
_LOGGER.debug("Not exposing %s because it has no capabilities",
|
||||
entity.entity_id)
|
||||
continue
|
||||
endpoint['capabilities'] = alexa_capabilities
|
||||
discovery_endpoints.append(endpoint)
|
||||
|
||||
return api_message(
|
||||
@@ -216,7 +549,7 @@ def async_api_discovery(hass, config, request):
|
||||
|
||||
|
||||
def extract_entity(funct):
|
||||
"""Decorator for extract entity object from request."""
|
||||
"""Decorate for extract entity object from request."""
|
||||
@asyncio.coroutine
|
||||
def async_api_entity_wrapper(hass, config, request):
|
||||
"""Process a turn on request."""
|
||||
@@ -240,12 +573,14 @@ def extract_entity(funct):
|
||||
def async_api_turn_on(hass, config, request, entity):
|
||||
"""Process a turn on request."""
|
||||
domain = entity.domain
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
service = SERVICE_TURN_ON
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
|
||||
yield from hass.services.async_call(domain, service, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -259,9 +594,13 @@ def async_api_turn_off(hass, config, request, entity):
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
service = SERVICE_TURN_OFF
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
|
||||
yield from hass.services.async_call(domain, service, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -276,7 +615,7 @@ def async_api_set_brightness(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -300,7 +639,7 @@ def async_api_adjust_brightness(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -321,14 +660,14 @@ def async_api_set_color(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
else:
|
||||
xyz = color_util.color_RGB_to_xy(*rgb)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
|
||||
light.ATTR_BRIGHTNESS: xyz[2],
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -343,7 +682,7 @@ def async_api_set_color_temperature(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_KELVIN: kelvin,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -361,7 +700,7 @@ def async_api_decrease_color_temp(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -371,7 +710,7 @@ def async_api_decrease_color_temp(hass, config, request, entity):
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_increase_color_temp(hass, config, request, entity):
|
||||
"""Process a increase color temperature request."""
|
||||
"""Process an increase color temperature request."""
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
||||
|
||||
@@ -379,7 +718,7 @@ def async_api_increase_color_temp(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -388,12 +727,54 @@ def async_api_increase_color_temp(hass, config, request, entity):
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_activate(hass, config, request, entity):
|
||||
"""Process a activate request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
"""Process an activate request."""
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
else:
|
||||
domain = entity.domain
|
||||
|
||||
return api_message(request)
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
|
||||
payload = {
|
||||
'cause': {'type': _Cause.VOICE_INTERACTION},
|
||||
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
|
||||
}
|
||||
|
||||
return api_message(
|
||||
request,
|
||||
name='ActivationStarted',
|
||||
namespace='Alexa.SceneController',
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_deactivate(hass, config, request, entity):
|
||||
"""Process a deactivate request."""
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
else:
|
||||
domain = entity.domain
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
|
||||
payload = {
|
||||
'cause': {'type': _Cause.VOICE_INTERACTION},
|
||||
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
|
||||
}
|
||||
|
||||
return api_message(
|
||||
request,
|
||||
name='DeactivationStarted',
|
||||
namespace='Alexa.SceneController',
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
|
||||
@@ -421,8 +802,8 @@ def async_api_set_percentage(hass, config, request, entity):
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = percentage
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -469,8 +850,8 @@ def async_api_adjust_percentage(hass, config, request, entity):
|
||||
|
||||
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -482,7 +863,7 @@ def async_api_lock(hass, config, request, entity):
|
||||
"""Process a lock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -495,7 +876,7 @@ def async_api_unlock(hass, config, request, entity):
|
||||
"""Process a unlock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -512,8 +893,9 @@ def async_api_set_volume(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -540,9 +922,9 @@ def async_api_adjust_volume(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -559,9 +941,9 @@ def async_api_set_mute(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_MUTE,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_MUTE,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -575,8 +957,9 @@ def async_api_play(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -590,8 +973,9 @@ def async_api_pause(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -605,8 +989,9 @@ def async_api_stop(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -620,9 +1005,9 @@ def async_api_next(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -636,8 +1021,30 @@ def async_api_previous(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa', 'ReportState'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_reportstate(hass, config, request, entity):
|
||||
"""Process a ReportState request."""
|
||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp_property = {
|
||||
'namespace': 'Alexa.TemperatureSensor',
|
||||
'name': 'temperature',
|
||||
'value': {
|
||||
'value': float(entity.state),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
},
|
||||
}
|
||||
|
||||
return api_message(
|
||||
request,
|
||||
name='StateReport',
|
||||
context={'properties': [temp_property]}
|
||||
)
|
||||
|
||||
@@ -89,6 +89,7 @@ def setup(hass, config):
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera
|
||||
|
||||
hass.data[DATA_AMCREST] = {}
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
@@ -126,22 +127,34 @@ def setup(hass, config):
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
||||
camera, name, authentication, ffmpeg_arguments, stream_source,
|
||||
resolution)
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_AUTHENTICATION: authentication,
|
||||
CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments,
|
||||
CONF_NAME: name,
|
||||
CONF_RESOLUTION: resolution,
|
||||
CONF_STREAM_SOURCE: stream_source,
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestDevice(object):
|
||||
"""Representation of a base Amcrest discovery device."""
|
||||
|
||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
||||
stream_source, resolution):
|
||||
"""Initialize the entity."""
|
||||
self.device = camera
|
||||
self.name = name
|
||||
self.authentication = authentication
|
||||
self.ffmpeg_arguments = ffmpeg_arguments
|
||||
self.stream_source = stream_source
|
||||
self.resolution = resolution
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.const import (
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
@@ -293,10 +294,11 @@ class APIServicesView(HomeAssistantView):
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
|
||||
@ha.callback
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Get registered services."""
|
||||
return self.json(async_services_json(request.app['hass']))
|
||||
services = yield from async_services_json(request.app['hass'])
|
||||
return self.json(services)
|
||||
|
||||
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
@@ -355,10 +357,12 @@ class APITemplateView(HomeAssistantView):
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
descriptions = yield from async_get_all_descriptions(hass)
|
||||
return [{"domain": key, "services": value}
|
||||
for key, value in hass.services.async_services().items()]
|
||||
for key, value in descriptions.items()]
|
||||
|
||||
|
||||
def async_events_json(hass):
|
||||
|
||||
@@ -4,21 +4,20 @@ Support for Apple TV.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/apple_tv/
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from typing import Sequence, TypeVar, Union
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from typing import Union, TypeVar, Sequence
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.components.discovery import SERVICE_APPLE_TV
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.8']
|
||||
REQUIREMENTS = ['pyatv==0.3.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,9 +60,9 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_CREDENTIALS, default=None): cv.string,
|
||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean,
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -142,7 +141,7 @@ def async_setup(hass, config):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
"""Handler for service calls."""
|
||||
"""Handle service calls."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if service.service == SERVICE_SCAN:
|
||||
@@ -169,7 +168,7 @@ def async_setup(hass, config):
|
||||
|
||||
@asyncio.coroutine
|
||||
def atv_discovered(service, info):
|
||||
"""Setup an Apple TV that was auto discovered."""
|
||||
"""Set up an Apple TV that was auto discovered."""
|
||||
yield from _setup_atv(hass, {
|
||||
CONF_NAME: info['name'],
|
||||
CONF_HOST: info['host'],
|
||||
@@ -183,18 +182,12 @@ def async_setup(hass, config):
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||
descriptions.get(SERVICE_SCAN),
|
||||
schema=APPLE_TV_SCAN_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
|
||||
descriptions.get(SERVICE_AUTHENTICATE),
|
||||
schema=APPLE_TV_AUTHENTICATE_SCHEMA)
|
||||
|
||||
return True
|
||||
@@ -202,7 +195,7 @@ def async_setup(hass, config):
|
||||
|
||||
@asyncio.coroutine
|
||||
def _setup_atv(hass, atv_config):
|
||||
"""Setup an Apple TV."""
|
||||
"""Set up an Apple TV."""
|
||||
import pyatv
|
||||
name = atv_config.get(CONF_NAME)
|
||||
host = atv_config.get(CONF_HOST)
|
||||
@@ -253,7 +246,7 @@ class AppleTVPowerManager:
|
||||
|
||||
@property
|
||||
def turned_on(self):
|
||||
"""If device is on or off."""
|
||||
"""Return true if device is on or off."""
|
||||
return self._is_on
|
||||
|
||||
def set_power_on(self, value):
|
||||
|
||||
@@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.1.0']
|
||||
REQUIREMENTS = ['pyarlo==0.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
"""Support for Asterisk Voicemail interface."""
|
||||
"""
|
||||
Support for Asterisk Voicemail interface.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/asterisk_mbox/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import (CONF_HOST,
|
||||
CONF_PORT, CONF_PASSWORD)
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import (async_dispatcher_connect,
|
||||
async_dispatcher_send)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, async_dispatcher_send)
|
||||
|
||||
REQUIREMENTS = ['asterisk_mbox==0.4.0']
|
||||
|
||||
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
|
||||
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'asterisk_mbox'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
|
||||
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT): int,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_PORT): int,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -43,7 +43,7 @@ def setup(hass, config):
|
||||
|
||||
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
|
||||
|
||||
discovery.load_platform(hass, "mailbox", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -68,15 +68,14 @@ class AsteriskData(object):
|
||||
from asterisk_mbox.commands import CMD_MESSAGE_LIST
|
||||
|
||||
if command == CMD_MESSAGE_LIST:
|
||||
_LOGGER.info("AsteriskVM sent updated message list")
|
||||
self.messages = sorted(msg,
|
||||
key=lambda item: item['info']['origtime'],
|
||||
reverse=True)
|
||||
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE,
|
||||
self.messages)
|
||||
_LOGGER.debug("AsteriskVM sent updated message list")
|
||||
self.messages = sorted(
|
||||
msg, key=lambda item: item['info']['origtime'], reverse=True)
|
||||
async_dispatcher_send(
|
||||
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
|
||||
|
||||
@callback
|
||||
def _request_messages(self):
|
||||
"""Handle changes to the mailbox."""
|
||||
_LOGGER.info("Requesting message list")
|
||||
_LOGGER.debug("Requesting message list")
|
||||
self.client.messages()
|
||||
|
||||
@@ -7,14 +7,12 @@ https://home-assistant.io/components/automation/
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import CoreState
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
|
||||
@@ -166,11 +164,6 @@ def async_setup(hass, config):
|
||||
|
||||
yield from _async_process_config(hass, config, component)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def trigger_service_handler(service_call):
|
||||
"""Handle automation triggers."""
|
||||
@@ -216,20 +209,20 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA)
|
||||
schema=TRIGGER_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA)
|
||||
schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||
descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA)
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, turn_onoff_service_handler,
|
||||
descriptions.get(service), schema=SERVICE_SCHEMA)
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -345,10 +338,9 @@ class AutomationEntity(ToggleEntity):
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_remove(self):
|
||||
"""Remove automation from HASS."""
|
||||
def async_will_remove_from_hass(self):
|
||||
"""Remove listeners when removing automation from HASS."""
|
||||
yield from self.async_turn_off()
|
||||
yield from super().async_remove()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_enable(self):
|
||||
|
||||
@@ -37,8 +37,8 @@ def async_trigger(hass, config, action):
|
||||
above = config.get(CONF_ABOVE)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
async_remove_track_same = None
|
||||
already_triggered = False
|
||||
unsub_track_same = {}
|
||||
entities_triggered = set()
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
@@ -63,8 +63,6 @@ def async_trigger(hass, config, action):
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal already_triggered, async_remove_track_same
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
@@ -81,16 +79,18 @@ def async_trigger(hass, config, action):
|
||||
|
||||
matching = check_numeric_state(entity, from_s, to_s)
|
||||
|
||||
if matching and not already_triggered:
|
||||
if not matching:
|
||||
entities_triggered.discard(entity)
|
||||
elif entity not in entities_triggered:
|
||||
entities_triggered.add(entity)
|
||||
|
||||
if time_delta:
|
||||
async_remove_track_same = async_track_same_state(
|
||||
unsub_track_same[entity] = async_track_same_state(
|
||||
hass, time_delta, call_action, entity_ids=entity_id,
|
||||
async_check_same_func=check_numeric_state)
|
||||
else:
|
||||
call_action()
|
||||
|
||||
already_triggered = matching
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
|
||||
@@ -98,7 +98,8 @@ def async_trigger(hass, config, action):
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
if async_remove_track_same:
|
||||
async_remove_track_same() # pylint: disable=not-callable
|
||||
for async_remove in unsub_track_same.values():
|
||||
async_remove()
|
||||
unsub_track_same.clear()
|
||||
|
||||
return async_remove
|
||||
|
||||
@@ -35,13 +35,11 @@ def async_trigger(hass, config, action):
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
||||
async_remove_track_same = None
|
||||
unsub_track_same = {}
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal async_remove_track_same
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
@@ -57,14 +55,14 @@ def async_trigger(hass, config, action):
|
||||
|
||||
# Ignore changes to state attributes if from/to is in use
|
||||
if (not match_all and from_s is not None and to_s is not None and
|
||||
from_s.last_changed == to_s.last_changed):
|
||||
from_s.state == to_s.state):
|
||||
return
|
||||
|
||||
if not time_delta:
|
||||
call_action()
|
||||
return
|
||||
|
||||
async_remove_track_same = async_track_same_state(
|
||||
unsub_track_same[entity] = async_track_same_state(
|
||||
hass, time_delta, call_action,
|
||||
lambda _, _2, to_state: to_state.state == to_s.state,
|
||||
entity_ids=entity_id)
|
||||
@@ -76,7 +74,8 @@ def async_trigger(hass, config, action):
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
if async_remove_track_same:
|
||||
async_remove_track_same() # pylint: disable=not-callable
|
||||
for async_remove in unsub_track_same.values():
|
||||
async_remove()
|
||||
unsub_track_same.clear()
|
||||
|
||||
return async_remove
|
||||
|
||||
@@ -4,25 +4,20 @@ Support for Axis devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/axis/
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_TRIGGER_TIME, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.const import (
|
||||
ATTR_LOCATION, ATTR_TRIPPED, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['axis==14']
|
||||
|
||||
@@ -83,10 +78,10 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
configurator = hass.components.configurator
|
||||
|
||||
def configuration_callback(callback_data):
|
||||
"""Called when config is submitted."""
|
||||
"""Call when configuration is submitted."""
|
||||
if CONF_INCLUDE not in callback_data:
|
||||
configurator.notify_errors(request_id,
|
||||
"Functionality mandatory.")
|
||||
configurator.notify_errors(
|
||||
request_id, "Functionality mandatory.")
|
||||
return False
|
||||
|
||||
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
|
||||
@@ -98,18 +93,20 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
try:
|
||||
device_config = DEVICE_SCHEMA(callback_data)
|
||||
except vol.Invalid:
|
||||
configurator.notify_errors(request_id,
|
||||
"Bad input, please check spelling.")
|
||||
configurator.notify_errors(
|
||||
request_id, "Bad input, please check spelling.")
|
||||
return False
|
||||
|
||||
if setup_device(hass, config, device_config):
|
||||
config_file = _read_config(hass)
|
||||
del device_config['events']
|
||||
del device_config['signal']
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
config_file[serialnumber] = dict(device_config)
|
||||
_write_config(hass, config_file)
|
||||
save_json(hass.config.path(CONFIG_FILE), config_file)
|
||||
configurator.request_done(request_id)
|
||||
else:
|
||||
configurator.notify_errors(request_id,
|
||||
"Failed to register, please try again.")
|
||||
configurator.notify_errors(
|
||||
request_id, "Failed to register, please try again.")
|
||||
return False
|
||||
|
||||
title = '{} ({})'.format(name, host)
|
||||
@@ -147,7 +144,7 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Common setup for Axis devices."""
|
||||
"""Set up for Axis devices."""
|
||||
def _shutdown(call): # pylint: disable=unused-argument
|
||||
"""Stop the event stream on shutdown."""
|
||||
for serialnumber, device in AXIS_DEVICES.items():
|
||||
@@ -157,13 +154,13 @@ def setup(hass, config):
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
|
||||
|
||||
def axis_device_discovered(service, discovery_info):
|
||||
"""Called when axis devices has been found."""
|
||||
"""Call when axis devices has been found."""
|
||||
host = discovery_info[CONF_HOST]
|
||||
name = discovery_info['hostname']
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
|
||||
if serialnumber not in AXIS_DEVICES:
|
||||
config_file = _read_config(hass)
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
if serialnumber in config_file:
|
||||
# Device config previously saved to file
|
||||
try:
|
||||
@@ -173,8 +170,8 @@ def setup(hass, config):
|
||||
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
||||
return False
|
||||
if not setup_device(hass, config, device_config):
|
||||
_LOGGER.error("Couldn\'t set up %s",
|
||||
device_config[CONF_NAME])
|
||||
_LOGGER.error(
|
||||
"Couldn't set up %s", device_config[CONF_NAME])
|
||||
else:
|
||||
# New device, create configuration request for UI
|
||||
request_configuration(hass, config, name, host, serialnumber)
|
||||
@@ -193,11 +190,7 @@ def setup(hass, config):
|
||||
if CONF_NAME not in device_config:
|
||||
device_config[CONF_NAME] = device
|
||||
if not setup_device(hass, config, device_config):
|
||||
_LOGGER.error("Couldn\'t set up %s", device_config[CONF_NAME])
|
||||
|
||||
# Services to communicate with device.
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
_LOGGER.error("Couldn't set up %s", device_config[CONF_NAME])
|
||||
|
||||
def vapix_service(call):
|
||||
"""Service to send a message."""
|
||||
@@ -209,24 +202,21 @@ def setup(hass, config):
|
||||
call.data[SERVICE_PARAM])
|
||||
hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response)
|
||||
return True
|
||||
_LOGGER.info("Couldn\'t find device %s", call.data[CONF_NAME])
|
||||
_LOGGER.info("Couldn't find device %s", call.data[CONF_NAME])
|
||||
return False
|
||||
|
||||
# Register service with Home Assistant.
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_VAPIX_CALL,
|
||||
vapix_service,
|
||||
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
|
||||
schema=SERVICE_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
def setup_device(hass, config, device_config):
|
||||
"""Set up device."""
|
||||
"""Set up an Axis device."""
|
||||
from axis import AxisDevice
|
||||
|
||||
def signal_callback(action, event):
|
||||
"""Callback to configure events when initialized on event stream."""
|
||||
"""Call to configure events when initialized on event stream."""
|
||||
if action == 'add':
|
||||
event_config = {
|
||||
CONF_EVENT: event,
|
||||
@@ -235,11 +225,8 @@ def setup_device(hass, config, device_config):
|
||||
CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME]
|
||||
}
|
||||
component = event.event_platform
|
||||
discovery.load_platform(hass,
|
||||
component,
|
||||
DOMAIN,
|
||||
event_config,
|
||||
config)
|
||||
discovery.load_platform(
|
||||
hass, component, DOMAIN, event_config, config)
|
||||
|
||||
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE],
|
||||
EVENT_TYPES))
|
||||
@@ -250,7 +237,7 @@ def setup_device(hass, config, device_config):
|
||||
|
||||
if device.serial_number is None:
|
||||
# If there is no serial number a connection could not be made
|
||||
_LOGGER.error("Couldn\'t connect to %s", device_config[CONF_HOST])
|
||||
_LOGGER.error("Couldn't connect to %s", device_config[CONF_HOST])
|
||||
return False
|
||||
|
||||
for component in device_config[CONF_INCLUDE]:
|
||||
@@ -262,11 +249,8 @@ def setup_device(hass, config, device_config):
|
||||
CONF_USERNAME: device_config[CONF_USERNAME],
|
||||
CONF_PASSWORD: device_config[CONF_PASSWORD]
|
||||
}
|
||||
discovery.load_platform(hass,
|
||||
component,
|
||||
DOMAIN,
|
||||
camera_config,
|
||||
config)
|
||||
discovery.load_platform(
|
||||
hass, component, DOMAIN, camera_config, config)
|
||||
|
||||
AXIS_DEVICES[device.serial_number] = device
|
||||
if event_types:
|
||||
@@ -274,34 +258,15 @@ def setup_device(hass, config, device_config):
|
||||
return True
|
||||
|
||||
|
||||
def _read_config(hass):
|
||||
"""Read Axis config."""
|
||||
path = hass.config.path(CONFIG_FILE)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return {}
|
||||
|
||||
with open(path) as f_handle:
|
||||
# Guard against empty file
|
||||
return json.loads(f_handle.read() or '{}')
|
||||
|
||||
|
||||
def _write_config(hass, config):
|
||||
"""Write Axis config."""
|
||||
data = json.dumps(config)
|
||||
with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(data)
|
||||
|
||||
|
||||
class AxisDeviceEvent(Entity):
|
||||
"""Representation of a Axis device event."""
|
||||
|
||||
def __init__(self, event_config):
|
||||
"""Initialize the event."""
|
||||
self.axis_event = event_config[CONF_EVENT]
|
||||
self._name = '{}_{}_{}'.format(event_config[CONF_NAME],
|
||||
self.axis_event.event_type,
|
||||
self.axis_event.id)
|
||||
self._name = '{}_{}_{}'.format(
|
||||
event_config[CONF_NAME], self.axis_event.event_type,
|
||||
self.axis_event.id)
|
||||
self.location = event_config[ATTR_LOCATION]
|
||||
self.axis_event.callback = self._update_callback
|
||||
|
||||
@@ -322,7 +287,7 @@ class AxisDeviceEvent(Entity):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
"""Return the polling state. No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
|
||||
@@ -20,22 +20,28 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
DEVICE_CLASSES = [
|
||||
'cold', # On means cold (or too cold)
|
||||
'connectivity', # On means connection present, Off = no connection
|
||||
'gas', # CO, CO2, etc.
|
||||
'heat', # On means hot (or too hot)
|
||||
'light', # Lightness threshold
|
||||
'moisture', # Specifically a wetness sensor
|
||||
'motion', # Motion sensor
|
||||
'moving', # On means moving, Off means stopped
|
||||
'occupancy', # On means occupied, Off means not occupied
|
||||
'opening', # Door, window, etc.
|
||||
'battery', # On means low, Off means normal
|
||||
'cold', # On means cold, Off means normal
|
||||
'connectivity', # On means connected, Off means disconnected
|
||||
'door', # On means open, Off means closed
|
||||
'garage_door', # On means open, Off means closed
|
||||
'gas', # On means gas detected, Off means no gas (clear)
|
||||
'heat', # On means hot, Off means normal
|
||||
'light', # On means light detected, Off means no light
|
||||
'moisture', # On means wet, Off means dry
|
||||
'motion', # On means motion detected, Off means no motion (clear)
|
||||
'moving', # On means moving, Off means not moving (stopped)
|
||||
'occupancy', # On means occupied, Off means not occupied (clear)
|
||||
'opening', # On means open, Off means closed
|
||||
'plug', # On means plugged in, Off means unplugged
|
||||
'power', # Power, over-current, etc
|
||||
'safety', # Generic on=unsafe, off=safe
|
||||
'smoke', # Smoke detector
|
||||
'sound', # On means sound detected, Off means no sound
|
||||
'power', # On means power detected, Off means no power
|
||||
'presence', # On means home, Off means away
|
||||
'problem', # On means problem detected, Off means no problem (OK)
|
||||
'safety', # On means unsafe, Off means safe
|
||||
'smoke', # On means smoke detected, Off means no smoke (clear)
|
||||
'sound', # On means sound detected, Off means no sound (clear)
|
||||
'vibration', # On means vibration detected, Off means no vibration
|
||||
'window', # On means open, Off means closed
|
||||
]
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
84
homeassistant/components/binary_sensor/ads.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Support for ADS binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation.
|
||||
https://home-assistant.io/components/binary_sensor.ads/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'ADS binary sensor'
|
||||
DEPENDENCIES = ['ads']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Binary Sensor platform for ADS."""
|
||||
ads_hub = hass.data.get(DATA_ADS)
|
||||
|
||||
ads_var = config.get(CONF_ADS_VAR)
|
||||
name = config.get(CONF_NAME)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
|
||||
add_devices([ads_sensor])
|
||||
|
||||
|
||||
class AdsBinarySensor(BinarySensorDevice):
|
||||
"""Representation of ADS binary sensors."""
|
||||
|
||||
def __init__(self, ads_hub, name, ads_var, device_class):
|
||||
"""Initialize ADS binary sensor."""
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._device_class = device_class or 'moving'
|
||||
self._ads_hub = ads_hub
|
||||
self.ads_var = ads_var
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
def update(name, value):
|
||||
"""Handle device notifications."""
|
||||
_LOGGER.debug('Variable %s changed its value to %d', name, value)
|
||||
self._state = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.hass.async_add_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the default name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False because entity pushes its state to HA."""
|
||||
return False
|
||||
@@ -7,39 +7,41 @@ https://home-assistant.io/components/binary_sensor.alarmdecoder/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
from homeassistant.components.alarmdecoder import (ZONE_SCHEMA,
|
||||
CONF_ZONES,
|
||||
CONF_ZONE_NAME,
|
||||
CONF_ZONE_TYPE,
|
||||
SIGNAL_ZONE_FAULT,
|
||||
SIGNAL_ZONE_RESTORE)
|
||||
|
||||
from homeassistant.components.alarmdecoder import (
|
||||
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
||||
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
||||
SIGNAL_RFX_MESSAGE)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_RF_BIT0 = 'rf_bit0'
|
||||
ATTR_RF_LOW_BAT = 'rf_low_battery'
|
||||
ATTR_RF_SUPERVISED = 'rf_supervised'
|
||||
ATTR_RF_BIT3 = 'rf_bit3'
|
||||
ATTR_RF_LOOP3 = 'rf_loop3'
|
||||
ATTR_RF_LOOP2 = 'rf_loop2'
|
||||
ATTR_RF_LOOP4 = 'rf_loop4'
|
||||
ATTR_RF_LOOP1 = 'rf_loop1'
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the AlarmDecoder binary sensor devices."""
|
||||
configured_zones = discovery_info[CONF_ZONES]
|
||||
|
||||
devices = []
|
||||
|
||||
for zone_num in configured_zones:
|
||||
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
zone_rfid = device_config_data.get(CONF_ZONE_RFID)
|
||||
device = AlarmDecoderBinarySensor(
|
||||
hass, zone_num, zone_name, zone_type)
|
||||
zone_num, zone_name, zone_type, zone_rfid)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
add_devices(devices)
|
||||
|
||||
return True
|
||||
|
||||
@@ -47,46 +49,52 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an AlarmDecoder binary sensor."""
|
||||
|
||||
def __init__(self, hass, zone_number, zone_name, zone_type):
|
||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._zone_type = zone_type
|
||||
self._state = 0
|
||||
self._state = None
|
||||
self._name = zone_name
|
||||
self._type = zone_type
|
||||
|
||||
_LOGGER.debug("Setup up zone: %s", self._name)
|
||||
self._rfid = zone_rfid
|
||||
self._rfstate = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback)
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_ZONE_RESTORE, self._restore_callback)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon for device by its type."""
|
||||
if "window" in self._name.lower():
|
||||
return "mdi:window-open" if self.is_on else "mdi:window-closed"
|
||||
|
||||
if self._type == 'smoke':
|
||||
return "mdi:fire"
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
if self._rfid and self._rfstate is not None:
|
||||
attr[ATTR_RF_BIT0] = True if self._rfstate & 0x01 else False
|
||||
attr[ATTR_RF_LOW_BAT] = True if self._rfstate & 0x02 else False
|
||||
attr[ATTR_RF_SUPERVISED] = True if self._rfstate & 0x04 else False
|
||||
attr[ATTR_RF_BIT3] = True if self._rfstate & 0x08 else False
|
||||
attr[ATTR_RF_LOOP3] = True if self._rfstate & 0x10 else False
|
||||
attr[ATTR_RF_LOOP2] = True if self._rfstate & 0x20 else False
|
||||
attr[ATTR_RF_LOOP4] = True if self._rfstate & 0x40 else False
|
||||
attr[ATTR_RF_LOOP1] = True if self._rfstate & 0x80 else False
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
@@ -97,16 +105,20 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@callback
|
||||
def _fault_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 1
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _restore_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 0
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _rfx_message_callback(self, message):
|
||||
"""Update RF state."""
|
||||
if self._rfid and message and message.serial_number == self._rfid:
|
||||
self._rfstate = message.value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -4,13 +4,12 @@ Support for Axis binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.axis/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.axis import (AxisDeviceEvent)
|
||||
from homeassistant.const import (CONF_TRIGGER_TIME)
|
||||
from homeassistant.components.axis import AxisDeviceEvent
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_TRIGGER_TIME
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
@@ -20,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Axis device event."""
|
||||
"""Set up the Axis binary devices."""
|
||||
add_devices([AxisBinarySensor(hass, discovery_info)], True)
|
||||
|
||||
|
||||
@@ -28,7 +27,7 @@ class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
|
||||
"""Representation of a binary Axis event."""
|
||||
|
||||
def __init__(self, hass, event_config):
|
||||
"""Initialize the binary sensor."""
|
||||
"""Initialize the Axis binary sensor."""
|
||||
self.hass = hass
|
||||
self._state = False
|
||||
self._delay = event_config[CONF_TRIGGER_TIME]
|
||||
@@ -56,7 +55,7 @@ class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
|
||||
# Set timer to wait until updating the state
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
_LOGGER.debug("%s Called delayed (%s sec) update.",
|
||||
_LOGGER.debug("%s called delayed (%s sec) update",
|
||||
self._name, self._delay)
|
||||
self.schedule_update_ha_state()
|
||||
self._timer = None
|
||||
|
||||
13
homeassistant/components/binary_sensor/concord232.py
Executable file → Normal file
@@ -15,7 +15,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
REQUIREMENTS = ['concord232==0.15']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
sensors = []
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Initializing Client")
|
||||
_LOGGER.debug("Initializing client")
|
||||
client = concord232_client.Client('http://{}:{}'.format(host, port))
|
||||
client.zones = client.list_zones()
|
||||
client.last_zone_update = datetime.datetime.now()
|
||||
@@ -62,6 +62,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return False
|
||||
|
||||
# The order of zones returned by client.list_zones() can vary.
|
||||
# When the zones are not named, this can result in the same entity
|
||||
# name mapping to different sensors in an unpredictable way. Sort
|
||||
# the zones by zone number to prevent this.
|
||||
|
||||
client.zones.sort(key=lambda zone: zone['number'])
|
||||
|
||||
for zone in client.zones:
|
||||
_LOGGER.info("Loading Zone found: %s", zone['name'])
|
||||
if zone['number'] not in exclude:
|
||||
@@ -118,7 +125,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
# True means "faulted" or "open" or "abnormal state"
|
||||
return bool(self._zone['state'] == 'Normal')
|
||||
return bool(self._zone['state'] != 'Normal')
|
||||
|
||||
def update(self):
|
||||
"""Get updated stats from API."""
|
||||
|
||||
97
homeassistant/components/binary_sensor/deconz.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Support for deCONZ binary sensor.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
|
||||
DEPENDENCIES = ['deconz']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
sensors = hass.data[DECONZ_DATA].sensors
|
||||
entities = []
|
||||
|
||||
for key in sorted(sensors.keys(), key=int):
|
||||
sensor = sensors[key]
|
||||
if sensor and sensor.type in DECONZ_BINARY_SENSOR:
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
|
||||
|
||||
class DeconzBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a binary sensor."""
|
||||
|
||||
def __init__(self, sensor):
|
||||
"""Set up sensor and add update callback to get data from websocket."""
|
||||
self._sensor = sensor
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self._sensor.register_async_callback(self.async_update_callback)
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
"""Update the sensor's state.
|
||||
|
||||
If reason is that state is updated,
|
||||
or reachable has changed or battery has changed.
|
||||
"""
|
||||
if reason['state'] or \
|
||||
'reachable' in reason['attr'] or \
|
||||
'battery' in reason['attr']:
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._sensor.is_tripped
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._sensor.name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the sensor."""
|
||||
return self._sensor.sensor_class
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._sensor.sensor_icon
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if sensor is available."""
|
||||
return self._sensor.reachable
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
from pydeconz.sensor import PRESENCE
|
||||
attr = {
|
||||
ATTR_BATTERY_LEVEL: self._sensor.battery,
|
||||
}
|
||||
if self._sensor.type == PRESENCE:
|
||||
attr['dark'] = self._sensor.dark
|
||||
return attr
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Support for reading binary states from a DoorBird video doorbell."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DEPENDENCIES = ['doorbird']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_MIN_UPDATE_INTERVAL = timedelta(milliseconds=250)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
"doorbell": {
|
||||
"name": "Doorbell Ringing",
|
||||
"icon": {
|
||||
True: "bell-ring",
|
||||
False: "bell",
|
||||
None: "bell-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the DoorBird binary sensor component."""
|
||||
device = hass.data.get(DOORBIRD_DOMAIN)
|
||||
add_devices([DoorBirdBinarySensor(device, "doorbell")], True)
|
||||
|
||||
|
||||
class DoorBirdBinarySensor(BinarySensorDevice):
|
||||
"""A binary sensor of a DoorBird device."""
|
||||
|
||||
def __init__(self, device, sensor_type):
|
||||
"""Initialize a binary sensor on a DoorBird device."""
|
||||
self._device = device
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Get the name of the sensor."""
|
||||
return SENSOR_TYPES[self._sensor_type]["name"]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Get an icon to display."""
|
||||
state_icon = SENSOR_TYPES[self._sensor_type]["icon"][self._state]
|
||||
return "mdi:{}".format(state_icon)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Get the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
@Throttle(_MIN_UPDATE_INTERVAL)
|
||||
def update(self):
|
||||
"""Pull the latest value from the device."""
|
||||
self._state = self._device.doorbell_state()
|
||||
@@ -238,6 +238,5 @@ class FlicButton(BinarySensorDevice):
|
||||
import pyflic
|
||||
|
||||
if connection_status == pyflic.ConnectionStatus.Disconnected:
|
||||
_LOGGER.info("Button (%s) disconnected. Reason: %s",
|
||||
self.address, disconnect_reason)
|
||||
self.remove()
|
||||
_LOGGER.warning("Button (%s) disconnected. Reason: %s",
|
||||
self.address, disconnect_reason)
|
||||
|
||||
63
homeassistant/components/binary_sensor/hive.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Support for the Hive devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.hive/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.hive import DATA_HIVE
|
||||
|
||||
DEPENDENCIES = ['hive']
|
||||
|
||||
DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion',
|
||||
'contactsensor': 'opening'}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Hive sensor devices."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
session = hass.data.get(DATA_HIVE)
|
||||
|
||||
add_devices([HiveBinarySensorEntity(session, discovery_info)])
|
||||
|
||||
|
||||
class HiveBinarySensorEntity(BinarySensorDevice):
|
||||
"""Representation of a Hive binary sensor."""
|
||||
|
||||
def __init__(self, hivesession, hivedevice):
|
||||
"""Initialize the hive sensor."""
|
||||
self.node_id = hivedevice["Hive_NodeID"]
|
||||
self.node_name = hivedevice["Hive_NodeName"]
|
||||
self.device_type = hivedevice["HA_DeviceType"]
|
||||
self.node_device_type = hivedevice["Hive_DeviceType"]
|
||||
self.session = hivesession
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
|
||||
self.session.entities.append(self)
|
||||
|
||||
def handle_update(self, updatesource):
|
||||
"""Handle the new update request."""
|
||||
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEVICETYPE_DEVICE_CLASS.get(self.node_device_type)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self.node_name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.session.sensor.get_state(self.node_id,
|
||||
self.node_device_type)
|
||||
|
||||
def update(self):
|
||||
"""Update all Node data frome Hive."""
|
||||
self.session.core.update_data(self.node_id)
|
||||
@@ -25,6 +25,7 @@ SENSOR_TYPES_CLASS = {
|
||||
'RemoteMotion': None,
|
||||
'WeatherSensor': None,
|
||||
'TiltSensor': None,
|
||||
'PresenceIP': 'motion',
|
||||
}
|
||||
|
||||
|
||||
|
||||
95
homeassistant/components/binary_sensor/ihc.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""IHC binary sensor platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ihc/
|
||||
"""
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.components.ihc import (
|
||||
validate_name, IHC_DATA, IHC_CONTROLLER, IHC_INFO)
|
||||
from homeassistant.components.ihc.const import CONF_INVERTING
|
||||
from homeassistant.components.ihc.ihcdevice import IHCDevice
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_TYPE, CONF_ID, CONF_BINARY_SENSORS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['ihc']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_BINARY_SENSORS, default=[]):
|
||||
vol.All(cv.ensure_list, [
|
||||
vol.All({
|
||||
vol.Required(CONF_ID): cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_TYPE, default=None): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_INVERTING, default=False): cv.boolean,
|
||||
}, validate_name)
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the IHC binary sensor platform."""
|
||||
ihc_controller = hass.data[IHC_DATA][IHC_CONTROLLER]
|
||||
info = hass.data[IHC_DATA][IHC_INFO]
|
||||
devices = []
|
||||
if discovery_info:
|
||||
for name, device in discovery_info.items():
|
||||
ihc_id = device['ihc_id']
|
||||
product_cfg = device['product_cfg']
|
||||
product = device['product']
|
||||
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
|
||||
product_cfg[CONF_TYPE],
|
||||
product_cfg[CONF_INVERTING],
|
||||
product)
|
||||
devices.append(sensor)
|
||||
else:
|
||||
binary_sensors = config[CONF_BINARY_SENSORS]
|
||||
for sensor_cfg in binary_sensors:
|
||||
ihc_id = sensor_cfg[CONF_ID]
|
||||
name = sensor_cfg[CONF_NAME]
|
||||
sensor_type = sensor_cfg[CONF_TYPE]
|
||||
inverting = sensor_cfg[CONF_INVERTING]
|
||||
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
|
||||
sensor_type, inverting)
|
||||
devices.append(sensor)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class IHCBinarySensor(IHCDevice, BinarySensorDevice):
|
||||
"""IHC Binary Sensor.
|
||||
|
||||
The associated IHC resource can be any in or output from a IHC product
|
||||
or function block, but it must be a boolean ON/OFF resources.
|
||||
"""
|
||||
|
||||
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
|
||||
sensor_type: str, inverting: bool, product: Element=None):
|
||||
"""Initialize the IHC binary sensor."""
|
||||
super().__init__(ihc_controller, name, ihc_id, info, product)
|
||||
self._state = None
|
||||
self._sensor_type = sensor_type
|
||||
self.inverting = inverting
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on/open."""
|
||||
return self._state
|
||||
|
||||
def on_ihc_change(self, ihc_id, value):
|
||||
"""IHC resource has changed."""
|
||||
if self.inverting:
|
||||
self._state = not value
|
||||
else:
|
||||
self._state = value
|
||||
self.schedule_update_ha_state()
|
||||
@@ -4,67 +4,364 @@ Support for ISY994 binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.isy994/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Callable # noqa
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
ISYDevice)
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
False: STATE_OFF,
|
||||
True: STATE_ON,
|
||||
ISY_DEVICE_TYPES = {
|
||||
'moisture': ['16.8', '16.13', '16.14'],
|
||||
'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'],
|
||||
'motion': ['16.1', '16.4', '16.5', '16.3']
|
||||
}
|
||||
|
||||
UOM = ['2', '78']
|
||||
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 binary sensor platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
devices_by_nid = {}
|
||||
child_nodes = []
|
||||
|
||||
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
|
||||
states=STATES):
|
||||
devices.append(ISYBinarySensorDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
if node.parent_node is None:
|
||||
device = ISYBinarySensorDevice(node)
|
||||
devices.append(device)
|
||||
devices_by_nid[node.nid] = device
|
||||
else:
|
||||
devices.append(ISYBinarySensorProgram(program.name, status))
|
||||
# We'll process the child nodes last, to ensure all parent nodes
|
||||
# have been processed
|
||||
child_nodes.append(node)
|
||||
|
||||
for node in child_nodes:
|
||||
try:
|
||||
parent_device = devices_by_nid[node.parent_node.nid]
|
||||
except KeyError:
|
||||
_LOGGER.error("Node %s has a parent node %s, but no device "
|
||||
"was created for the parent. Skipping.",
|
||||
node.nid, node.parent_nid)
|
||||
else:
|
||||
device_type = _detect_device_type(node)
|
||||
subnode_id = int(node.nid[-1])
|
||||
if device_type == 'opening':
|
||||
# Door/window sensors use an optional "negative" node
|
||||
if subnode_id == 4:
|
||||
# Subnode 4 is the heartbeat node, which we will represent
|
||||
# as a separate binary_sensor
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
elif subnode_id == 2:
|
||||
parent_device.add_negative_node(node)
|
||||
elif device_type == 'moisture':
|
||||
# Moisure nodes have a subnode 2, but we ignore it because it's
|
||||
# just the inverse of the primary node.
|
||||
if subnode_id == 4:
|
||||
# Heartbeat node
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
else:
|
||||
# We don't yet have any special logic for other sensor types,
|
||||
# so add the nodes as individual devices
|
||||
device = ISYBinarySensorDevice(node)
|
||||
devices.append(device)
|
||||
|
||||
for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]:
|
||||
devices.append(ISYBinarySensorProgram(name, status))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor device."""
|
||||
def _detect_device_type(node) -> str:
|
||||
try:
|
||||
device_type = node.type
|
||||
except AttributeError:
|
||||
# The type attribute didn't exist in the ISY's API response
|
||||
return None
|
||||
|
||||
split_type = device_type.split('.')
|
||||
for device_class, ids in ISY_DEVICE_TYPES.items():
|
||||
if '{}.{}'.format(split_type[0], split_type[1]) in ids:
|
||||
return device_class
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _is_val_unknown(val):
|
||||
"""Determine if a number value represents UNKNOWN from PyISY."""
|
||||
return val == -1*float('inf')
|
||||
|
||||
|
||||
class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor device.
|
||||
|
||||
Often times, a single device is represented by multiple nodes in the ISY,
|
||||
allowing for different nuances in how those devices report their on and
|
||||
off events. This class turns those multiple nodes in to a single Hass
|
||||
entity and handles both ways that ISY binary sensors can work.
|
||||
"""
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
self._negative_node = None
|
||||
self._heartbeat_device = None
|
||||
self._device_class_from_type = _detect_device_type(self._node)
|
||||
# pylint: disable=protected-access
|
||||
if _is_val_unknown(self._node.status._val):
|
||||
self._computed_state = None
|
||||
else:
|
||||
self._computed_state = bool(self._node.status._val)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node and subnode event emitters."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
self._node.controlEvents.subscribe(self._positive_node_control_handler)
|
||||
|
||||
if self._negative_node is not None:
|
||||
self._negative_node.controlEvents.subscribe(
|
||||
self._negative_node_control_handler)
|
||||
|
||||
def add_heartbeat_device(self, device) -> None:
|
||||
"""Register a heartbeat device for this sensor.
|
||||
|
||||
The heartbeat node beats on its own, but we can gain a little
|
||||
reliability by considering any node activity for this sensor
|
||||
to be a heartbeat as well.
|
||||
"""
|
||||
self._heartbeat_device = device
|
||||
|
||||
def _heartbeat(self) -> None:
|
||||
"""Send a heartbeat to our heartbeat device, if we have one."""
|
||||
if self._heartbeat_device is not None:
|
||||
self._heartbeat_device.heartbeat()
|
||||
|
||||
def add_negative_node(self, child) -> None:
|
||||
"""Add a negative node to this binary sensor device.
|
||||
|
||||
The negative node is a node that can receive the 'off' events
|
||||
for the sensor, depending on device configuration and type.
|
||||
"""
|
||||
self._negative_node = child
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if not _is_val_unknown(self._negative_node.status._val):
|
||||
# If the negative node has a value, it means the negative node is
|
||||
# in use for this device. Therefore, we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
|
||||
def _negative_node_control_handler(self, event: object) -> None:
|
||||
"""Handle an "On" control event from the "negative" node."""
|
||||
if event == 'DON':
|
||||
_LOGGER.debug("Sensor %s turning Off via the Negative node "
|
||||
"sending a DON command", self.name)
|
||||
self._computed_state = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
def _positive_node_control_handler(self, event: object) -> None:
|
||||
"""Handle On and Off control event coming from the primary node.
|
||||
|
||||
Depending on device configuration, sometimes only On events
|
||||
will come to this node, with the negative node representing Off
|
||||
events
|
||||
"""
|
||||
if event == 'DON':
|
||||
_LOGGER.debug("Sensor %s turning On via the Primary node "
|
||||
"sending a DON command", self.name)
|
||||
self._computed_state = True
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
if event == 'DOF':
|
||||
_LOGGER.debug("Sensor %s turning Off via the Primary node "
|
||||
"sending a DOF command", self.name)
|
||||
self._computed_state = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore primary node status updates.
|
||||
|
||||
We listen directly to the Control events on all nodes for this
|
||||
device.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
"""Get the current value of the device.
|
||||
|
||||
Insteon leak sensors set their primary node to On when the state is
|
||||
DRY, not WET, so we invert the binary state if the user indicates
|
||||
that it is a moisture sensor.
|
||||
"""
|
||||
if self._computed_state is None:
|
||||
# Do this first so we don't invert None on moisture sensors
|
||||
return None
|
||||
|
||||
if self.device_class == 'moisture':
|
||||
return not self._computed_state
|
||||
|
||||
return self._computed_state
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 binary sensor device is on.
|
||||
|
||||
Note: This method will return false if the current state is UNKNOWN
|
||||
"""
|
||||
return bool(self.value)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
if self._computed_state is None:
|
||||
return None
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Return the class of this device.
|
||||
|
||||
This was discovered by parsing the device type code during init
|
||||
"""
|
||||
return self._device_class_from_type
|
||||
|
||||
|
||||
class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
|
||||
"""Representation of the battery state of an ISY994 sensor."""
|
||||
|
||||
def __init__(self, node, parent_device) -> None:
|
||||
"""Initialize the ISY994 binary sensor device."""
|
||||
super().__init__(node)
|
||||
self._computed_state = None
|
||||
self._parent_device = parent_device
|
||||
self._heartbeat_timer = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node and subnode event emitters."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
self._node.controlEvents.subscribe(
|
||||
self._heartbeat_node_control_handler)
|
||||
|
||||
# Start the timer on bootup, so we can change from UNKNOWN to ON
|
||||
self._restart_timer()
|
||||
|
||||
def _heartbeat_node_control_handler(self, event: object) -> None:
|
||||
"""Update the heartbeat timestamp when an On event is sent."""
|
||||
if event == 'DON':
|
||||
self.heartbeat()
|
||||
|
||||
def heartbeat(self):
|
||||
"""Mark the device as online, and restart the 25 hour timer.
|
||||
|
||||
This gets called when the heartbeat node beats, but also when the
|
||||
parent sensor sends any events, as we can trust that to mean the device
|
||||
is online. This mitigates the risk of false positives due to a single
|
||||
missed heartbeat event.
|
||||
"""
|
||||
self._computed_state = False
|
||||
self._restart_timer()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _restart_timer(self):
|
||||
"""Restart the 25 hour timer."""
|
||||
try:
|
||||
self._heartbeat_timer()
|
||||
self._heartbeat_timer = None
|
||||
except TypeError:
|
||||
# No heartbeat timer is active
|
||||
pass
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@callback
|
||||
def timer_elapsed(now) -> None:
|
||||
"""Heartbeat missed; set state to indicate dead battery."""
|
||||
self._computed_state = True
|
||||
self._heartbeat_timer = None
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
point_in_time = dt_util.utcnow() + timedelta(hours=25)
|
||||
_LOGGER.debug("Timer starting. Now: %s Then: %s",
|
||||
dt_util.utcnow(), point_in_time)
|
||||
|
||||
self._heartbeat_timer = async_track_point_in_utc_time(
|
||||
self.hass, timer_elapsed, point_in_time)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore node status updates.
|
||||
|
||||
We listen directly to the Control events for this device.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
"""Get the current value of this sensor."""
|
||||
return self._computed_state
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 binary sensor device is on.
|
||||
|
||||
Note: This method will return false if the current state is UNKNOWN
|
||||
"""
|
||||
return bool(self.value)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
if self._computed_state is None:
|
||||
return None
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Get the class of this device."""
|
||||
return 'battery'
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Get the state attributes for the device."""
|
||||
attr = super().device_state_attributes
|
||||
attr['parent_entity_id'] = self._parent_device.entity_id
|
||||
return attr
|
||||
|
||||
|
||||
class ISYBinarySensorProgram(ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor program.
|
||||
|
||||
This does not need all of the subnode logic in the device version of binary
|
||||
sensors.
|
||||
"""
|
||||
|
||||
def __init__(self, name, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor program."""
|
||||
super().__init__(node)
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 binary sensor device is on."""
|
||||
return bool(self.value)
|
||||
|
||||
|
||||
class ISYBinarySensorProgram(ISYBinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor program."""
|
||||
|
||||
def __init__(self, name, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor program."""
|
||||
ISYBinarySensorDevice.__init__(self, node)
|
||||
self._name = name
|
||||
|
||||
@@ -5,12 +5,13 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.knx/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES, \
|
||||
KNXAutomation
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, \
|
||||
BinarySensorDevice
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.components.knx import (
|
||||
ATTR_DISCOVER_DEVICES, DATA_KNX, KNXAutomation)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -53,20 +54,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up binary sensor(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data \
|
||||
or not hass.data[DATA_KNX].initialized:
|
||||
return False
|
||||
if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized:
|
||||
return
|
||||
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
else:
|
||||
async_add_devices_config(hass, config, async_add_devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
||||
@@ -80,7 +77,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
||||
|
||||
@callback
|
||||
def async_add_devices_config(hass, config, async_add_devices):
|
||||
"""Set up binary senor for KNX platform configured within plattform."""
|
||||
"""Set up binary senor for KNX platform configured within platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
import xknx
|
||||
binary_sensor = xknx.devices.BinarySensor(
|
||||
@@ -108,7 +105,7 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a KNX binary sensor."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialization of KNXBinarySensor."""
|
||||
"""Initialize of KNX binary sensor."""
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
@@ -119,7 +116,7 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
"""Callback after device was updated."""
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
@@ -129,6 +126,11 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.hass.data[DATA_KNX].connected
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed within KNX."""
|
||||
|
||||
@@ -36,7 +36,7 @@ class MaxCubeShutter(BinarySensorDevice):
|
||||
def __init__(self, hass, name, rf_address):
|
||||
"""Initialize MAX! Cube BinarySensorDevice."""
|
||||
self._name = name
|
||||
self._sensor_type = 'opening'
|
||||
self._sensor_type = 'window'
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@@ -17,19 +17,15 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
|
||||
CONF_DEVICE_CLASS)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_QOS, valid_subscribe_topic)
|
||||
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PAYLOAD_AVAILABLE = 'payload_available'
|
||||
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
|
||||
|
||||
DEFAULT_NAME = 'MQTT Binary sensor'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_AVAILABLE = 'online'
|
||||
DEFAULT_PAYLOAD_NOT_AVAILABLE = 'offline'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
@@ -38,12 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_AVAILABILITY_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_AVAILABLE): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string,
|
||||
})
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -70,31 +61,29 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)])
|
||||
|
||||
|
||||
class MqttBinarySensor(BinarySensorDevice):
|
||||
class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, availability_topic, device_class,
|
||||
qos, payload_on, payload_off, payload_available,
|
||||
payload_not_available, value_template):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._state_topic = state_topic
|
||||
self._availability_topic = availability_topic
|
||||
self._available = True if availability_topic is None else False
|
||||
self._device_class = device_class
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._payload_available = payload_available
|
||||
self._payload_not_available = payload_not_available
|
||||
self._qos = qos
|
||||
self._template = value_template
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
"""Subscribe mqtt events."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def state_message_received(topic, payload, qos):
|
||||
"""Handle a new received MQTT state message."""
|
||||
@@ -111,21 +100,6 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, state_message_received, self._qos)
|
||||
|
||||
@callback
|
||||
def availability_message_received(topic, payload, qos):
|
||||
"""Handle a new received MQTT availability message."""
|
||||
if payload == self._payload_available:
|
||||
self._available = True
|
||||
elif payload == self._payload_not_available:
|
||||
self._available = False
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._availability_topic is not None:
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._availability_topic,
|
||||
availability_message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
@@ -136,11 +110,6 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the binary sensor is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
||||
85
homeassistant/components/binary_sensor/mychevy.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Support for MyChevy sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mychevy/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.mychevy import (
|
||||
EVBinarySensorConfig, DOMAIN as MYCHEVY_DOMAIN, UPDATE_TOPIC
|
||||
)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
ENTITY_ID_FORMAT, BinarySensorDevice)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSORS = [
|
||||
EVBinarySensorConfig("Plugged In", "plugged_in", "plug")
|
||||
]
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MyChevy sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
sensors = []
|
||||
hub = hass.data[MYCHEVY_DOMAIN]
|
||||
for sconfig in SENSORS:
|
||||
sensors.append(EVBinarySensor(hub, sconfig))
|
||||
|
||||
async_add_devices(sensors)
|
||||
|
||||
|
||||
class EVBinarySensor(BinarySensorDevice):
|
||||
"""Base EVSensor class.
|
||||
|
||||
The only real difference between sensors is which units and what
|
||||
attribute from the car object they are returning. All logic can be
|
||||
built with just setting subclass attributes.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, connection, config):
|
||||
"""Initialize sensor with car connection."""
|
||||
self._conn = connection
|
||||
self._name = config.name
|
||||
self._attr = config.attr
|
||||
self._type = config.device_class
|
||||
self._is_on = None
|
||||
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(
|
||||
'{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name)))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if on."""
|
||||
return self._is_on
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
UPDATE_TOPIC, self.async_update_callback)
|
||||
|
||||
@callback
|
||||
def async_update_callback(self):
|
||||
"""Update state."""
|
||||
if self._conn.car is not None:
|
||||
self._is_on = getattr(self._conn.car, self._attr, None)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
@@ -5,21 +5,20 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mysensors/
|
||||
"""
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.binary_sensor import (DEVICE_CLASSES, DOMAIN,
|
||||
BinarySensorDevice)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES, DOMAIN, BinarySensorDevice)
|
||||
from homeassistant.const import STATE_ON
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for binary sensors."""
|
||||
"""Set up the MySensors platform for binary sensors."""
|
||||
mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsBinarySensor,
|
||||
add_devices=add_devices)
|
||||
|
||||
|
||||
class MySensorsBinarySensor(
|
||||
mysensors.MySensorsEntity, BinarySensorDevice):
|
||||
"""Represent the value of a MySensors Binary Sensor child node."""
|
||||
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
||||
"""Representation of a MySensors Binary Sensor child node."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.mystrom/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice, DOMAIN)
|
||||
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY
|
||||
|
||||
@@ -37,7 +37,7 @@ class MyStromView(HomeAssistantView):
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""The GET request received from a myStrom button."""
|
||||
"""Handle the GET request received from a myStrom button."""
|
||||
res = yield from self._handle(request.app['hass'], request.query)
|
||||
return res
|
||||
|
||||
|
||||
@@ -5,18 +5,17 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.raspihats/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_DEVICE_CLASS, DEVICE_DEFAULT_NAME
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, BinarySensorDevice
|
||||
)
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.components.raspihats import (
|
||||
CONF_I2C_HATS, CONF_BOARD, CONF_ADDRESS, CONF_CHANNELS, CONF_INDEX,
|
||||
CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException
|
||||
)
|
||||
CONF_ADDRESS, CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX,
|
||||
CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS, CONF_NAME, DEVICE_DEFAULT_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the raspihats binary_sensor devices."""
|
||||
"""Set up the raspihats binary_sensor devices."""
|
||||
I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER]
|
||||
binary_sensors = []
|
||||
i2c_hat_configs = config.get(CONF_I2C_HATS)
|
||||
@@ -65,39 +64,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
)
|
||||
)
|
||||
except I2CHatsException as ex:
|
||||
_LOGGER.error(
|
||||
"Failed to register " + board + "I2CHat@" + hex(address) + " "
|
||||
+ str(ex)
|
||||
)
|
||||
_LOGGER.error("Failed to register %s I2CHat@%s %s",
|
||||
board, hex(address), str(ex))
|
||||
add_devices(binary_sensors)
|
||||
|
||||
|
||||
class I2CHatBinarySensor(BinarySensorDevice):
|
||||
"""Represents a binary sensor that uses a I2C-HAT digital input."""
|
||||
"""Representation of a binary sensor that uses a I2C-HAT digital input."""
|
||||
|
||||
I2C_HATS_MANAGER = None
|
||||
|
||||
def __init__(self, address, channel, name, invert_logic, device_class):
|
||||
"""Initialize sensor."""
|
||||
"""Initialize the raspihats sensor."""
|
||||
self._address = address
|
||||
self._channel = channel
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._invert_logic = invert_logic
|
||||
self._device_class = device_class
|
||||
self._state = self.I2C_HATS_MANAGER.read_di(
|
||||
self._address,
|
||||
self._channel
|
||||
)
|
||||
self._address, self._channel)
|
||||
|
||||
def online_callback():
|
||||
"""Callback fired when board is online."""
|
||||
"""Call fired when board is online."""
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.I2C_HATS_MANAGER.register_online_callback(
|
||||
self._address,
|
||||
self._channel,
|
||||
online_callback
|
||||
)
|
||||
self._address, self._channel, online_callback)
|
||||
|
||||
def edge_callback(state):
|
||||
"""Read digital input state."""
|
||||
@@ -105,10 +97,7 @@ class I2CHatBinarySensor(BinarySensorDevice):
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.I2C_HATS_MANAGER.register_di_callback(
|
||||
self._address,
|
||||
self._channel,
|
||||
edge_callback
|
||||
)
|
||||
self._address, self._channel, edge_callback)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@@ -122,7 +111,7 @@ class I2CHatBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling not needed for this sensor."""
|
||||
"""No polling needed for this sensor."""
|
||||
return False
|
||||
|
||||
@property
|
||||
|
||||
@@ -98,6 +98,11 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
"""Return the class of this sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return the availability of this sensor."""
|
||||
return self.rest.data is not None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
||||
@@ -1,41 +1,50 @@
|
||||
"""
|
||||
Support for RFXtrx binary sensors.
|
||||
|
||||
Lighting4 devices (sensors based on PT2262 encoder) are supported and
|
||||
tested. Other types may need some work.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rfxtrx/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import rfxtrx
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES,
|
||||
CONF_FIRE_EVENT, CONF_OFF_DELAY)
|
||||
from homeassistant.const import (
|
||||
CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_DEVICE_CLASS, CONF_NAME)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import event as evt
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_OFF_DELAY, ATTR_FIREEVENT,
|
||||
ATTR_DATA_BITS, CONF_DEVICES
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["rfxtrx"]
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required("platform"): rfxtrx.DOMAIN,
|
||||
vol.Optional(CONF_DEVICES, default={}): vol.All(
|
||||
dict, rfxtrx.valid_binary_sensor),
|
||||
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICES, default={}): {
|
||||
cv.string: vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS, default=None):
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_OFF_DELAY, default=None):
|
||||
vol.Any(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DATA_BITS, default=None): cv.positive_int,
|
||||
vol.Optional(CONF_COMMAND_ON, default=None): cv.byte,
|
||||
vol.Optional(CONF_COMMAND_OFF, default=None): cv.byte
|
||||
})
|
||||
},
|
||||
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the Binary Sensor platform to rfxtrx."""
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Binary Sensor platform to RFXtrx."""
|
||||
import RFXtrx as rfxtrxmod
|
||||
sensors = []
|
||||
|
||||
@@ -46,30 +55,27 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
if device_id in rfxtrx.RFX_DEVICES:
|
||||
continue
|
||||
|
||||
if entity[ATTR_DATA_BITS] is not None:
|
||||
_LOGGER.info("Masked device id: %s",
|
||||
rfxtrx.get_pt2262_deviceid(device_id,
|
||||
entity[ATTR_DATA_BITS]))
|
||||
if entity[CONF_DATA_BITS] is not None:
|
||||
_LOGGER.debug(
|
||||
"Masked device id: %s", rfxtrx.get_pt2262_deviceid(
|
||||
device_id, entity[CONF_DATA_BITS]))
|
||||
|
||||
_LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)",
|
||||
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
|
||||
_LOGGER.debug("Add %s rfxtrx.binary_sensor (class %s)",
|
||||
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
|
||||
|
||||
device = RfxtrxBinarySensor(event, entity[ATTR_NAME],
|
||||
entity[CONF_DEVICE_CLASS],
|
||||
entity[ATTR_FIREEVENT],
|
||||
entity[ATTR_OFF_DELAY],
|
||||
entity[ATTR_DATA_BITS],
|
||||
entity[CONF_COMMAND_ON],
|
||||
entity[CONF_COMMAND_OFF])
|
||||
device = RfxtrxBinarySensor(
|
||||
event, entity[ATTR_NAME], entity[CONF_DEVICE_CLASS],
|
||||
entity[CONF_FIRE_EVENT], entity[CONF_OFF_DELAY],
|
||||
entity[CONF_DATA_BITS], entity[CONF_COMMAND_ON],
|
||||
entity[CONF_COMMAND_OFF])
|
||||
device.hass = hass
|
||||
sensors.append(device)
|
||||
rfxtrx.RFX_DEVICES[device_id] = device
|
||||
|
||||
add_devices_callback(sensors)
|
||||
add_devices(sensors)
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def binary_sensor_update(event):
|
||||
"""Callback for control updates from the RFXtrx gateway."""
|
||||
"""Call for control updates from the RFXtrx gateway."""
|
||||
if not isinstance(event, rfxtrxmod.ControlEvent):
|
||||
return
|
||||
|
||||
@@ -82,36 +88,33 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
if sensor is None:
|
||||
# Add the entity if not exists and automatic_add is True
|
||||
if not config[ATTR_AUTOMATIC_ADD]:
|
||||
if not config[CONF_AUTOMATIC_ADD]:
|
||||
return
|
||||
|
||||
if event.device.packettype == 0x13:
|
||||
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
|
||||
if poss_dev is not None:
|
||||
poss_id = slugify(poss_dev.event.device.id_string.lower())
|
||||
_LOGGER.info("Found possible matching deviceid %s.",
|
||||
poss_id)
|
||||
_LOGGER.debug(
|
||||
"Found possible matching device ID: %s", poss_id)
|
||||
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
sensor = RfxtrxBinarySensor(event, pkt_id)
|
||||
sensor.hass = hass
|
||||
rfxtrx.RFX_DEVICES[device_id] = sensor
|
||||
add_devices_callback([sensor])
|
||||
_LOGGER.info("Added binary sensor %s "
|
||||
"(Device_id: %s Class: %s Sub: %s)",
|
||||
pkt_id,
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype)
|
||||
add_devices([sensor])
|
||||
_LOGGER.info(
|
||||
"Added binary sensor %s (Device ID: %s Class: %s Sub: %s)",
|
||||
pkt_id, slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__, event.device.subtype)
|
||||
|
||||
elif not isinstance(sensor, RfxtrxBinarySensor):
|
||||
return
|
||||
else:
|
||||
_LOGGER.info("Binary sensor update "
|
||||
"(Device_id: %s Class: %s Sub: %s)",
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype)
|
||||
_LOGGER.debug(
|
||||
"Binary sensor update (Device ID: %s Class: %s Sub: %s)",
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__, event.device.subtype)
|
||||
|
||||
if sensor.is_lighting4:
|
||||
if sensor.data_bits is not None:
|
||||
@@ -131,22 +134,20 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
sensor.update_state(False)
|
||||
|
||||
sensor.delay_listener = evt.track_point_in_time(
|
||||
hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay
|
||||
)
|
||||
hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay)
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
# Subscribe to main RFXtrx events
|
||||
if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes,too-many-arguments
|
||||
class RfxtrxBinarySensor(BinarySensorDevice):
|
||||
"""An Rfxtrx binary sensor."""
|
||||
"""A representation of a RFXtrx binary sensor."""
|
||||
|
||||
def __init__(self, event, name, device_class=None,
|
||||
should_fire=False, off_delay=None, data_bits=None,
|
||||
cmd_on=None, cmd_off=None):
|
||||
"""Initialize the sensor."""
|
||||
"""Initialize the RFXtrx sensor."""
|
||||
self.event = event
|
||||
self._name = name
|
||||
self._should_fire_event = should_fire
|
||||
@@ -161,12 +162,9 @@ class RfxtrxBinarySensor(BinarySensorDevice):
|
||||
|
||||
if data_bits is not None:
|
||||
self._masked_id = rfxtrx.get_pt2262_deviceid(
|
||||
event.device.id_string.lower(),
|
||||
data_bits)
|
||||
|
||||
def __str__(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
event.device.id_string.lower(), data_bits)
|
||||
else:
|
||||
self._masked_id = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -8,18 +8,17 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.rpi_pfio as rpi_pfio
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
import homeassistant.components.rpi_pfio as rpi_pfio
|
||||
from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_INVERT_LOGIC = 'invert_logic'
|
||||
ATTR_SETTLE_TIME = 'settle_time'
|
||||
CONF_INVERT_LOGIC = 'invert_logic'
|
||||
CONF_PORTS = 'ports'
|
||||
CONF_SETTLE_TIME = 'settle_time'
|
||||
|
||||
DEFAULT_INVERT_LOGIC = False
|
||||
DEFAULT_SETTLE_TIME = 20
|
||||
@@ -27,27 +26,27 @@ DEFAULT_SETTLE_TIME = 20
|
||||
DEPENDENCIES = ['rpi_pfio']
|
||||
|
||||
PORT_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_NAME, default=None): cv.string,
|
||||
vol.Optional(ATTR_SETTLE_TIME, default=DEFAULT_SETTLE_TIME):
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_SETTLE_TIME, default=DEFAULT_SETTLE_TIME):
|
||||
cv.positive_int,
|
||||
vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean
|
||||
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PORTS, default={}): vol.Schema({
|
||||
cv.positive_int: PORT_SCHEMA
|
||||
cv.positive_int: PORT_SCHEMA,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the PiFace Digital Input devices."""
|
||||
"""Set up the PiFace Digital Input devices."""
|
||||
binary_sensors = []
|
||||
ports = config.get('ports')
|
||||
ports = config.get(CONF_PORTS)
|
||||
for port, port_entity in ports.items():
|
||||
name = port_entity[ATTR_NAME]
|
||||
settle_time = port_entity[ATTR_SETTLE_TIME] / 1000
|
||||
invert_logic = port_entity[ATTR_INVERT_LOGIC]
|
||||
name = port_entity[CONF_NAME]
|
||||
settle_time = port_entity[CONF_SETTLE_TIME] / 1000
|
||||
invert_logic = port_entity[CONF_INVERT_LOGIC]
|
||||
|
||||
binary_sensors.append(RPiPFIOBinarySensor(
|
||||
hass, port, name, settle_time, invert_logic))
|
||||
|
||||
@@ -4,46 +4,49 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.spc/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.spc import (
|
||||
ATTR_DISCOVER_DEVICES, DATA_REGISTRY)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import (STATE_UNAVAILABLE, STATE_ON, STATE_OFF)
|
||||
|
||||
from homeassistant.components.spc import ATTR_DISCOVER_DEVICES, DATA_REGISTRY
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPC_TYPE_TO_DEVICE_CLASS = {'0': 'motion',
|
||||
'1': 'opening',
|
||||
'3': 'smoke'}
|
||||
SPC_TYPE_TO_DEVICE_CLASS = {
|
||||
'0': 'motion',
|
||||
'1': 'opening',
|
||||
'3': 'smoke',
|
||||
}
|
||||
|
||||
|
||||
SPC_INPUT_TO_SENSOR_STATE = {'0': STATE_OFF,
|
||||
'1': STATE_ON}
|
||||
SPC_INPUT_TO_SENSOR_STATE = {
|
||||
'0': STATE_OFF,
|
||||
'1': STATE_ON,
|
||||
}
|
||||
|
||||
|
||||
def _get_device_class(spc_type):
|
||||
"""Get the device class."""
|
||||
return SPC_TYPE_TO_DEVICE_CLASS.get(spc_type, None)
|
||||
|
||||
|
||||
def _get_sensor_state(spc_input):
|
||||
"""Get the sensor state."""
|
||||
return SPC_INPUT_TO_SENSOR_STATE.get(spc_input, STATE_UNAVAILABLE)
|
||||
|
||||
|
||||
def _create_sensor(hass, zone):
|
||||
return SpcBinarySensor(zone_id=zone['id'],
|
||||
name=zone['zone_name'],
|
||||
state=_get_sensor_state(zone['input']),
|
||||
device_class=_get_device_class(zone['type']),
|
||||
spc_registry=hass.data[DATA_REGISTRY])
|
||||
"""Create a SPC sensor."""
|
||||
return SpcBinarySensor(
|
||||
zone_id=zone['id'], name=zone['zone_name'],
|
||||
state=_get_sensor_state(zone['input']),
|
||||
device_class=_get_device_class(zone['type']),
|
||||
spc_registry=hass.data[DATA_REGISTRY])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Initialize the platform."""
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the SPC binary sensor."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
return
|
||||
@@ -55,7 +58,7 @@ def async_setup_platform(hass, config, async_add_devices,
|
||||
|
||||
|
||||
class SpcBinarySensor(BinarySensorDevice):
|
||||
"""Represents a sensor based on an SPC zone."""
|
||||
"""Representation of a sensor based on a SPC zone."""
|
||||
|
||||
def __init__(self, zone_id, name, state, device_class, spc_registry):
|
||||
"""Initialize the sensor device."""
|
||||
@@ -74,7 +77,7 @@ class SpcBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the device."""
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
@@ -85,7 +88,7 @@ class SpcBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def hidden(self) -> bool:
|
||||
"""Whether the device is hidden by default."""
|
||||
# these type of sensors are probably mainly used for automations
|
||||
# These type of sensors are probably mainly used for automations
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -95,5 +98,5 @@ class SpcBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""The device class."""
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@@ -4,15 +4,15 @@ Support for Taps Affs.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.tapsaff/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME)
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['tapsaff==0.1.3']
|
||||
|
||||
@@ -67,7 +67,7 @@ class TapsAffData(object):
|
||||
"""Class for handling the data retrieval for pins."""
|
||||
|
||||
def __init__(self, location):
|
||||
"""Initialize the sensor."""
|
||||
"""Initialize the data object."""
|
||||
from tapsaff import TapsAff
|
||||
|
||||
self._is_taps_aff = None
|
||||
|
||||
@@ -38,6 +38,11 @@ SENSOR_SCHEMA = vol.Schema({
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
})
|
||||
|
||||
SENSOR_SCHEMA = vol.All(
|
||||
cv.deprecated(ATTR_ENTITY_ID),
|
||||
SENSOR_SCHEMA,
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ class TeslaBinarySensor(TeslaDevice, BinarySensorDevice):
|
||||
"""Implement an Tesla binary sensor for parking and charger."""
|
||||
|
||||
def __init__(self, tesla_device, controller, sensor_type):
|
||||
"""Initialisation of binary sensor."""
|
||||
"""Initialise of a Tesla binary sensor."""
|
||||
super().__init__(tesla_device, controller)
|
||||
self._state = False
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
|
||||
@@ -9,40 +9,48 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN,
|
||||
ATTR_ENTITY_ID, CONF_DEVICE_CLASS)
|
||||
ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_HYSTERESIS = 'hysteresis'
|
||||
ATTR_LOWER = 'lower'
|
||||
ATTR_POSITION = 'position'
|
||||
ATTR_SENSOR_VALUE = 'sensor_value'
|
||||
ATTR_THRESHOLD = 'threshold'
|
||||
ATTR_TYPE = 'type'
|
||||
ATTR_UPPER = 'upper'
|
||||
|
||||
CONF_HYSTERESIS = 'hysteresis'
|
||||
CONF_LOWER = 'lower'
|
||||
CONF_THRESHOLD = 'threshold'
|
||||
CONF_UPPER = 'upper'
|
||||
|
||||
DEFAULT_NAME = 'Threshold'
|
||||
DEFAULT_HYSTERESIS = 0.0
|
||||
|
||||
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER]
|
||||
POSITION_ABOVE = 'above'
|
||||
POSITION_BELOW = 'below'
|
||||
POSITION_IN_RANGE = 'in_range'
|
||||
POSITION_UNKNOWN = 'unknown'
|
||||
|
||||
TYPE_LOWER = 'lower'
|
||||
TYPE_RANGE = 'range'
|
||||
TYPE_UPPER = 'upper'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
|
||||
vol.Optional(
|
||||
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS):
|
||||
vol.Coerce(float),
|
||||
vol.Optional(CONF_LOWER): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_UPPER): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
@@ -51,47 +59,44 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Threshold sensor."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
name = config.get(CONF_NAME)
|
||||
threshold = config.get(CONF_THRESHOLD)
|
||||
lower = config.get(CONF_LOWER)
|
||||
upper = config.get(CONF_UPPER)
|
||||
hysteresis = config.get(CONF_HYSTERESIS)
|
||||
limit_type = config.get(CONF_TYPE)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_devices([ThresholdSensor(
|
||||
hass, entity_id, name, threshold,
|
||||
hysteresis, limit_type, device_class)
|
||||
], True)
|
||||
|
||||
return True
|
||||
hass, entity_id, name, lower, upper, hysteresis, device_class)], True)
|
||||
|
||||
|
||||
class ThresholdSensor(BinarySensorDevice):
|
||||
"""Representation of a Threshold sensor."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, threshold,
|
||||
hysteresis, limit_type, device_class):
|
||||
def __init__(self, hass, entity_id, name, lower, upper, hysteresis,
|
||||
device_class):
|
||||
"""Initialize the Threshold sensor."""
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self.is_upper = limit_type == 'upper'
|
||||
self._name = name
|
||||
self._threshold = threshold
|
||||
self._threshold_lower = lower
|
||||
self._threshold_upper = upper
|
||||
self._hysteresis = hysteresis
|
||||
self._device_class = device_class
|
||||
self._state = False
|
||||
self.sensor_value = 0
|
||||
|
||||
@callback
|
||||
self._state_position = None
|
||||
self._state = False
|
||||
self.sensor_value = None
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@callback
|
||||
def async_threshold_sensor_state_listener(
|
||||
entity, old_state, new_state):
|
||||
"""Handle sensor state changes."""
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
return
|
||||
|
||||
try:
|
||||
self.sensor_value = float(new_state.state)
|
||||
except ValueError:
|
||||
_LOGGER.error("State is not numerical")
|
||||
self.sensor_value = None if new_state.state == STATE_UNKNOWN \
|
||||
else float(new_state.state)
|
||||
except (ValueError, TypeError):
|
||||
self.sensor_value = None
|
||||
_LOGGER.warning("State is not numerical")
|
||||
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
@@ -118,23 +123,67 @@ class ThresholdSensor(BinarySensorDevice):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def threshold_type(self):
|
||||
"""Return the type of threshold this sensor represents."""
|
||||
if self._threshold_lower and self._threshold_upper:
|
||||
return TYPE_RANGE
|
||||
elif self._threshold_lower:
|
||||
return TYPE_LOWER
|
||||
elif self._threshold_upper:
|
||||
return TYPE_UPPER
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_SENSOR_VALUE: self.sensor_value,
|
||||
ATTR_THRESHOLD: self._threshold,
|
||||
ATTR_HYSTERESIS: self._hysteresis,
|
||||
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
|
||||
ATTR_LOWER: self._threshold_lower,
|
||||
ATTR_POSITION: self._state_position,
|
||||
ATTR_SENSOR_VALUE: self.sensor_value,
|
||||
ATTR_TYPE: self.threshold_type,
|
||||
ATTR_UPPER: self._threshold_upper,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
if self._hysteresis == 0 and self.sensor_value == self._threshold:
|
||||
def below(threshold):
|
||||
"""Determine if the sensor value is below a threshold."""
|
||||
return self.sensor_value < (threshold - self._hysteresis)
|
||||
|
||||
def above(threshold):
|
||||
"""Determine if the sensor value is above a threshold."""
|
||||
return self.sensor_value > (threshold + self._hysteresis)
|
||||
|
||||
if self.sensor_value is None:
|
||||
self._state_position = POSITION_UNKNOWN
|
||||
self._state = False
|
||||
elif self.sensor_value > (self._threshold + self._hysteresis):
|
||||
self._state = self.is_upper
|
||||
elif self.sensor_value < (self._threshold - self._hysteresis):
|
||||
self._state = not self.is_upper
|
||||
|
||||
elif self.threshold_type == TYPE_LOWER:
|
||||
if below(self._threshold_lower):
|
||||
self._state_position = POSITION_BELOW
|
||||
self._state = True
|
||||
elif above(self._threshold_lower):
|
||||
self._state_position = POSITION_ABOVE
|
||||
self._state = False
|
||||
|
||||
elif self.threshold_type == TYPE_UPPER:
|
||||
if above(self._threshold_upper):
|
||||
self._state_position = POSITION_ABOVE
|
||||
self._state = True
|
||||
elif below(self._threshold_upper):
|
||||
self._state_position = POSITION_BELOW
|
||||
self._state = False
|
||||
|
||||
elif self.threshold_type == TYPE_RANGE:
|
||||
if below(self._threshold_lower):
|
||||
self._state_position = POSITION_BELOW
|
||||
self._state = False
|
||||
if above(self._threshold_upper):
|
||||
self._state_position = POSITION_ABOVE
|
||||
self._state = False
|
||||
elif above(self._threshold_lower) and below(self._threshold_upper):
|
||||
self._state_position = POSITION_IN_RANGE
|
||||
self._state = True
|
||||
|
||||
@@ -11,21 +11,19 @@ import math
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||
BinarySensorDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_DEVICE_CLASS, CONF_ENTITY_ID,
|
||||
CONF_FRIENDLY_NAME, STATE_UNKNOWN)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME,
|
||||
CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.13.3']
|
||||
REQUIREMENTS = ['numpy==1.14.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,21 +34,21 @@ ATTR_INVERT = 'invert'
|
||||
ATTR_SAMPLE_DURATION = 'sample_duration'
|
||||
ATTR_SAMPLE_COUNT = 'sample_count'
|
||||
|
||||
CONF_SENSORS = 'sensors'
|
||||
CONF_ATTRIBUTE = 'attribute'
|
||||
CONF_INVERT = 'invert'
|
||||
CONF_MAX_SAMPLES = 'max_samples'
|
||||
CONF_MIN_GRADIENT = 'min_gradient'
|
||||
CONF_INVERT = 'invert'
|
||||
CONF_SAMPLE_DURATION = 'sample_duration'
|
||||
CONF_SENSORS = 'sensors'
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_MAX_SAMPLES, default=2): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_GRADIENT, default=0.0): vol.Coerce(float),
|
||||
vol.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SAMPLE_DURATION, default=0): cv.positive_int,
|
||||
})
|
||||
|
||||
@@ -129,11 +127,11 @@ class SensorTrend(BinarySensorDevice):
|
||||
return {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_FRIENDLY_NAME: self._name,
|
||||
ATTR_INVERT: self._invert,
|
||||
ATTR_GRADIENT: self._gradient,
|
||||
ATTR_INVERT: self._invert,
|
||||
ATTR_MIN_GRADIENT: self._min_gradient,
|
||||
ATTR_SAMPLE_DURATION: self._sample_duration,
|
||||
ATTR_SAMPLE_COUNT: len(self.samples),
|
||||
ATTR_SAMPLE_DURATION: self._sample_duration,
|
||||
}
|
||||
|
||||
@property
|
||||
|
||||
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Vera controller devices."""
|
||||
add_devices(
|
||||
VeraBinarySensor(device, VERA_CONTROLLER)
|
||||
for device in VERA_DEVICES['binary_sensor'])
|
||||
VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
|
||||
for device in hass.data[VERA_DEVICES]['binary_sensor'])
|
||||
|
||||
|
||||
class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
|
||||
@@ -6,15 +6,15 @@ https://home-assistant.io/components/binary_sensor.verisure/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.verisure import CONF_DOOR_WINDOW
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Verisure binary sensors."""
|
||||
"""Set up the Verisure binary sensors."""
|
||||
sensors = []
|
||||
hub.update_overview()
|
||||
|
||||
@@ -27,10 +27,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class VerisureDoorWindowSensor(BinarySensorDevice):
|
||||
"""Verisure door window sensor."""
|
||||
"""Representation of a Verisure door window sensor."""
|
||||
|
||||
def __init__(self, device_label):
|
||||
"""Initialize the modbus coil sensor."""
|
||||
"""Initialize the Verisure door window sensor."""
|
||||
self._device_label = device_label
|
||||
|
||||
@property
|
||||
|
||||
@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Vultr subscription (server) sensor."""
|
||||
"""Set up the Vultr subscription (server) binary sensor."""
|
||||
vultr = hass.data[DATA_VULTR]
|
||||
|
||||
subscription = config.get(CONF_SUBSCRIPTION)
|
||||
@@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if subscription not in vultr.data:
|
||||
_LOGGER.error("Subscription %s not found", subscription)
|
||||
return False
|
||||
return
|
||||
|
||||
add_devices([VultrBinarySensor(vultr, subscription, name)], True)
|
||||
|
||||
@@ -48,7 +48,7 @@ class VultrBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Vultr subscription sensor."""
|
||||
|
||||
def __init__(self, vultr, subscription, name):
|
||||
"""Initialize a new Vultr sensor."""
|
||||
"""Initialize a new Vultr binary sensor."""
|
||||
self._vultr = vultr
|
||||
self._name = name
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
from homeassistant.components.wink import DOMAIN, WinkDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,18 +16,18 @@ DEPENDENCIES = ['wink']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
'opened': 'opening',
|
||||
'brightness': 'light',
|
||||
'vibration': 'vibration',
|
||||
'loudness': 'sound',
|
||||
'noise': 'sound',
|
||||
'capturing_audio': 'sound',
|
||||
'liquid_detected': 'moisture',
|
||||
'motion': 'motion',
|
||||
'presence': 'occupancy',
|
||||
'capturing_video': None,
|
||||
'co_detected': 'gas',
|
||||
'liquid_detected': 'moisture',
|
||||
'loudness': 'sound',
|
||||
'motion': 'motion',
|
||||
'noise': 'sound',
|
||||
'opened': 'opening',
|
||||
'presence': 'occupancy',
|
||||
'smoke_detected': 'smoke',
|
||||
'capturing_video': None
|
||||
'vibration': 'vibration',
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
"""Call when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['binary_sensor'].append(self)
|
||||
|
||||
@property
|
||||
@@ -118,7 +118,7 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
"""Return the device state attributes."""
|
||||
return super().device_state_attributes
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
"""Return the device state attributes."""
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['test_activated'] = self.wink.test_activated()
|
||||
return _attributes
|
||||
@@ -138,11 +138,18 @@ class WinkHub(WinkBinarySensorDevice):
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
"""Return the device state attributes."""
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['update_needed'] = self.wink.update_needed()
|
||||
_attributes['firmware_version'] = self.wink.firmware_version()
|
||||
_attributes['pairing_mode'] = self.wink.pairing_mode()
|
||||
_kidde_code = self.wink.kidde_radio_code()
|
||||
if _kidde_code is not None:
|
||||
# The service call to set the Kidde code
|
||||
# takes a string of 1s and 0s so it makes
|
||||
# sense to display it to the user that way
|
||||
_formatted_kidde_code = "{:b}".format(_kidde_code).zfill(8)
|
||||
_attributes['kidde_radio_code'] = _formatted_kidde_code
|
||||
return _attributes
|
||||
|
||||
|
||||
@@ -170,7 +177,7 @@ class WinkButton(WinkBinarySensorDevice):
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
"""Return the device state attributes."""
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['pressed'] = self.wink.pressed()
|
||||
_attributes['long_pressed'] = self.wink.long_pressed()
|
||||
|
||||
@@ -17,17 +17,21 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.8.1']
|
||||
REQUIREMENTS = ['holidays==0.9.3']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Canada', 'CA',
|
||||
'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'England',
|
||||
'EuropeanCentralBank', 'ECB', 'TAR', 'Germany', 'DE',
|
||||
'Ireland', 'Isle of Man', 'Mexico', 'MX', 'Netherlands', 'NL',
|
||||
'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO',
|
||||
'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Spain',
|
||||
'ES', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales']
|
||||
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
|
||||
'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK',
|
||||
'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland',
|
||||
'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland',
|
||||
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
|
||||
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
|
||||
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
||||
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
|
||||
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
|
||||
'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US',
|
||||
'Wales']
|
||||
CONF_COUNTRY = 'country'
|
||||
CONF_PROVINCE = 'province'
|
||||
CONF_WORKDAYS = 'workdays'
|
||||
@@ -64,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
excludes = config.get(CONF_EXCLUDES)
|
||||
days_offset = config.get(CONF_OFFSET)
|
||||
|
||||
year = (datetime.now() + timedelta(days=days_offset)).year
|
||||
year = (get_date(datetime.today()) + timedelta(days=days_offset)).year
|
||||
obj_holidays = getattr(holidays, country)(years=year)
|
||||
|
||||
if province:
|
||||
@@ -99,6 +103,11 @@ def day_to_string(day):
|
||||
return None
|
||||
|
||||
|
||||
def get_date(date):
|
||||
"""Return date. Needed for testing."""
|
||||
return date
|
||||
|
||||
|
||||
class IsWorkdaySensor(BinarySensorDevice):
|
||||
"""Implementation of a Workday sensor."""
|
||||
|
||||
@@ -156,7 +165,7 @@ class IsWorkdaySensor(BinarySensorDevice):
|
||||
self._state = False
|
||||
|
||||
# Get iso day of the week (1 = Monday, 7 = Sunday)
|
||||
date = datetime.today() + timedelta(days=self._days_offset)
|
||||
date = get_date(datetime.today()) + timedelta(days=self._days_offset)
|
||||
day = date.isoweekday() - 1
|
||||
day_of_week = day_to_string(day)
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor):
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
if DENSITY in data:
|
||||
self._density = int(data.get(DENSITY))
|
||||
@@ -139,8 +139,16 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
if raw_data['cmd'] == 'heartbeat':
|
||||
_LOGGER.debug(
|
||||
'Skipping heartbeat of the motion sensor. '
|
||||
'It can introduce an incorrect state because of a firmware '
|
||||
'bug (https://github.com/home-assistant/home-assistant/pull/'
|
||||
'11631#issuecomment-357507744).')
|
||||
return
|
||||
|
||||
self._should_poll = False
|
||||
if NO_MOTION in data: # handle push from the hub
|
||||
self._no_motion_since = data[NO_MOTION]
|
||||
@@ -186,7 +194,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
self._should_poll = False
|
||||
if NO_CLOSE in data: # handle push from the hub
|
||||
@@ -219,7 +227,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
|
||||
XiaomiBinarySensor.__init__(self, device, 'Water Leak Sensor',
|
||||
xiaomi_hub, 'status', 'moisture')
|
||||
|
||||
def parse_data(self, data):
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
self._should_poll = False
|
||||
|
||||
@@ -256,7 +264,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
if DENSITY in data:
|
||||
self._density = int(data.get(DENSITY))
|
||||
@@ -293,7 +301,7 @@ class XiaomiButton(XiaomiBinarySensor):
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
@@ -343,7 +351,7 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
def parse_data(self, data, raw_data):
|
||||
"""Parse data sent by gateway."""
|
||||
if 'status' in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
|
||||
219
homeassistant/components/calendar/caldav.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Support for WebDav Calendar.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar.caldav/
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.calendar import (
|
||||
PLATFORM_SCHEMA, CalendarEventDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
REQUIREMENTS = ['caldav==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
CONF_CALENDARS = 'calendars'
|
||||
CONF_CUSTOM_CALENDARS = 'custom_calendars'
|
||||
CONF_CALENDAR = 'calendar'
|
||||
CONF_SEARCH = 'search'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Required(CONF_URL): vol.Url(),
|
||||
vol.Optional(CONF_CALENDARS, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Schema([
|
||||
cv.string
|
||||
])),
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_CUSTOM_CALENDARS, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Schema([
|
||||
vol.Schema({
|
||||
vol.Required(CONF_CALENDAR): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_SEARCH): cv.string,
|
||||
})
|
||||
]))
|
||||
})
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
"""Set up the WebDav Calendar platform."""
|
||||
import caldav
|
||||
|
||||
url = config.get(CONF_URL)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
client = caldav.DAVClient(url, None, username, password)
|
||||
|
||||
calendars = client.principal().calendars()
|
||||
|
||||
calendar_devices = []
|
||||
for calendar in list(calendars):
|
||||
# If a calendar name was given in the configuration,
|
||||
# ignore all the others
|
||||
if (config.get(CONF_CALENDARS)
|
||||
and calendar.name not in config.get(CONF_CALENDARS)):
|
||||
_LOGGER.debug("Ignoring calendar '%s'", calendar.name)
|
||||
continue
|
||||
|
||||
# Create additional calendars based on custom filtering rules
|
||||
for cust_calendar in config.get(CONF_CUSTOM_CALENDARS):
|
||||
# Check that the base calendar matches
|
||||
if cust_calendar.get(CONF_CALENDAR) != calendar.name:
|
||||
continue
|
||||
|
||||
device_data = {
|
||||
CONF_NAME: cust_calendar.get(CONF_NAME),
|
||||
CONF_DEVICE_ID: "{} {}".format(
|
||||
cust_calendar.get(CONF_CALENDAR),
|
||||
cust_calendar.get(CONF_NAME)),
|
||||
}
|
||||
|
||||
calendar_devices.append(
|
||||
WebDavCalendarEventDevice(
|
||||
hass, device_data, calendar, True,
|
||||
cust_calendar.get(CONF_SEARCH)))
|
||||
|
||||
# Create a default calendar if there was no custom one
|
||||
if not config.get(CONF_CUSTOM_CALENDARS):
|
||||
device_data = {
|
||||
CONF_NAME: calendar.name,
|
||||
CONF_DEVICE_ID: calendar.name
|
||||
}
|
||||
calendar_devices.append(
|
||||
WebDavCalendarEventDevice(hass, device_data, calendar)
|
||||
)
|
||||
|
||||
add_devices(calendar_devices)
|
||||
|
||||
|
||||
class WebDavCalendarEventDevice(CalendarEventDevice):
|
||||
"""A device for getting the next Task from a WebDav Calendar."""
|
||||
|
||||
def __init__(self, hass, device_data, calendar, all_day=False,
|
||||
search=None):
|
||||
"""Create the WebDav Calendar Event Device."""
|
||||
self.data = WebDavCalendarData(calendar, all_day, search)
|
||||
super().__init__(hass, device_data)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
if self.data.event is None:
|
||||
# No tasks, we don't REALLY need to show anything.
|
||||
return {}
|
||||
|
||||
attributes = super().device_state_attributes
|
||||
return attributes
|
||||
|
||||
|
||||
class WebDavCalendarData(object):
|
||||
"""Class to utilize the calendar dav client object to get next event."""
|
||||
|
||||
def __init__(self, calendar, include_all_day, search):
|
||||
"""Set up how we are going to search the WebDav calendar."""
|
||||
self.calendar = calendar
|
||||
self.include_all_day = include_all_day
|
||||
self.search = search
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
# We have to retrieve the results for the whole day as the server
|
||||
# won't return events that have already started
|
||||
results = self.calendar.date_search(
|
||||
dt.start_of_local_day(),
|
||||
dt.start_of_local_day() + timedelta(days=1)
|
||||
)
|
||||
|
||||
# dtstart can be a date or datetime depending if the event lasts a
|
||||
# whole day. Convert everything to datetime to be able to sort it
|
||||
results.sort(key=lambda x: self.to_datetime(
|
||||
x.instance.vevent.dtstart.value
|
||||
))
|
||||
|
||||
vevent = next((
|
||||
event.instance.vevent for event in results
|
||||
if (self.is_matching(event.instance.vevent, self.search)
|
||||
and (not self.is_all_day(event.instance.vevent)
|
||||
or self.include_all_day)
|
||||
and not self.is_over(event.instance.vevent))), None)
|
||||
|
||||
# If no matching event could be found
|
||||
if vevent is None:
|
||||
_LOGGER.debug(
|
||||
"No matching event found in the %d results for %s",
|
||||
len(results), self.calendar.name)
|
||||
self.event = None
|
||||
return True
|
||||
|
||||
# Populate the entity attributes with the event values
|
||||
self.event = {
|
||||
"summary": vevent.summary.value,
|
||||
"start": self.get_hass_date(vevent.dtstart.value),
|
||||
"end": self.get_hass_date(vevent.dtend.value),
|
||||
"location": self.get_attr_value(vevent, "location"),
|
||||
"description": self.get_attr_value(vevent, "description")
|
||||
}
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_matching(vevent, search):
|
||||
"""Return if the event matches the filter critera."""
|
||||
if search is None:
|
||||
return True
|
||||
|
||||
pattern = re.compile(search)
|
||||
return (hasattr(vevent, "summary")
|
||||
and pattern.match(vevent.summary.value)
|
||||
or hasattr(vevent, "location")
|
||||
and pattern.match(vevent.location.value)
|
||||
or hasattr(vevent, "description")
|
||||
and pattern.match(vevent.description.value))
|
||||
|
||||
@staticmethod
|
||||
def is_all_day(vevent):
|
||||
"""Return if the event last the whole day."""
|
||||
return not isinstance(vevent.dtstart.value, datetime)
|
||||
|
||||
@staticmethod
|
||||
def is_over(vevent):
|
||||
"""Return if the event is over."""
|
||||
return dt.now() > WebDavCalendarData.to_datetime(vevent.dtend.value)
|
||||
|
||||
@staticmethod
|
||||
def get_hass_date(obj):
|
||||
"""Return if the event matches."""
|
||||
if isinstance(obj, datetime):
|
||||
return {"dateTime": obj.isoformat()}
|
||||
|
||||
return {"date": obj.isoformat()}
|
||||
|
||||
@staticmethod
|
||||
def to_datetime(obj):
|
||||
"""Return a datetime."""
|
||||
if isinstance(obj, datetime):
|
||||
return obj
|
||||
return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min))
|
||||
|
||||
@staticmethod
|
||||
def get_attr_value(obj, attribute):
|
||||
"""Return the value of the attribute if defined."""
|
||||
if hasattr(obj, attribute):
|
||||
return getattr(obj, attribute).value
|
||||
return None
|
||||
0
homeassistant/components/calendar/demo.py
Executable file → Normal file
@@ -4,31 +4,27 @@ Support for Todoist task management (https://todoist.com).
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar.todoist/
|
||||
"""
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.calendar import (
|
||||
CalendarEventDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.google import (
|
||||
CONF_DEVICE_ID)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
CONF_ID, CONF_NAME, CONF_TOKEN)
|
||||
DOMAIN, PLATFORM_SCHEMA, CalendarEventDevice)
|
||||
from homeassistant.components.google import CONF_DEVICE_ID
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.util import dt
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
REQUIREMENTS = ['todoist-python==7.0.17']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'todoist'
|
||||
|
||||
CONF_EXTRA_PROJECTS = 'custom_projects'
|
||||
CONF_PROJECT_DUE_DATE = 'due_date_days'
|
||||
CONF_PROJECT_LABEL_WHITELIST = 'labels'
|
||||
CONF_PROJECT_WHITELIST = 'include_projects'
|
||||
|
||||
# Calendar Platform: Does this calendar event last all day?
|
||||
ALL_DAY = 'all_day'
|
||||
@@ -80,21 +76,16 @@ SUMMARY = 'summary'
|
||||
# Todoist API: Fetch all Tasks
|
||||
TASKS = 'items'
|
||||
|
||||
SERVICE_NEW_TASK = 'new_task'
|
||||
SERVICE_NEW_TASK = 'todoist_new_task'
|
||||
|
||||
NEW_TASK_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONTENT): cv.string,
|
||||
vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower),
|
||||
vol.Optional(LABELS): cv.ensure_list_csv,
|
||||
vol.Optional(PRIORITY): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1, max=4)),
|
||||
vol.Optional(DUE_DATE): cv.string
|
||||
vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
|
||||
vol.Optional(DUE_DATE): cv.string,
|
||||
})
|
||||
|
||||
CONF_EXTRA_PROJECTS = 'custom_projects'
|
||||
CONF_PROJECT_DUE_DATE = 'due_date_days'
|
||||
CONF_PROJECT_WHITELIST = 'include_projects'
|
||||
CONF_PROJECT_LABEL_WHITELIST = 'labels'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TOKEN): cv.string,
|
||||
vol.Optional(CONF_EXTRA_PROJECTS, default=[]):
|
||||
@@ -114,8 +105,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Todoist platform."""
|
||||
# Check token:
|
||||
"""Set up the Todoist platform."""
|
||||
token = config.get(CONF_TOKEN)
|
||||
|
||||
# Look up IDs based on (lowercase) names.
|
||||
@@ -178,12 +168,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices(project_devices)
|
||||
|
||||
# Services:
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def handle_new_task(call):
|
||||
"""Called when a user creates a new Todoist Task from HASS."""
|
||||
"""Call when a user creates a new Todoist Task from HASS."""
|
||||
project_name = call.data[PROJECT_NAME]
|
||||
project_id = project_id_lookup[project_name]
|
||||
|
||||
@@ -215,7 +201,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.debug("Created Todoist task: %s", call.data[CONTENT])
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_NEW_TASK, handle_new_task,
|
||||
descriptions[DOMAIN][SERVICE_NEW_TASK],
|
||||
schema=NEW_TASK_SERVICE_SCHEMA)
|
||||
|
||||
|
||||
@@ -536,8 +521,7 @@ class TodoistProjectData(object):
|
||||
# Let's set our "due date" to tomorrow
|
||||
self.event[END] = {
|
||||
DATETIME: (
|
||||
datetime.utcnow() +
|
||||
timedelta(days=1)
|
||||
datetime.utcnow() + timedelta(days=1)
|
||||
).strftime(DATE_STR_FORMAT)
|
||||
}
|
||||
_LOGGER.debug("Updated %s", self._name)
|
||||
|
||||
@@ -12,7 +12,6 @@ from datetime import timedelta
|
||||
import logging
|
||||
import hashlib
|
||||
from random import SystemRandom
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
@@ -21,7 +20,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -126,15 +124,15 @@ def async_setup(hass, config):
|
||||
"""Set up the camera component."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.http.register_view(CameraImageView(component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(component.entities))
|
||||
hass.http.register_view(CameraImageView(component))
|
||||
hass.http.register_view(CameraMjpegStream(component))
|
||||
|
||||
yield from component.async_setup(config)
|
||||
|
||||
@callback
|
||||
def update_tokens(time):
|
||||
"""Update tokens of the entities."""
|
||||
for entity in component.entities.values():
|
||||
for entity in component.entities:
|
||||
entity.async_update_token()
|
||||
hass.async_add_job(entity.async_update_ha_state())
|
||||
|
||||
@@ -190,19 +188,14 @@ def async_setup(hass, config):
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write image to file: %s", err)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_ENABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DISABLE_MOTION, async_handle_camera_service,
|
||||
descriptions.get(SERVICE_DISABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA)
|
||||
schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SNAPSHOT, async_handle_snapshot_service,
|
||||
descriptions.get(SERVICE_SNAPSHOT),
|
||||
schema=CAMERA_SERVICE_SNAPSHOT)
|
||||
|
||||
return True
|
||||
@@ -365,14 +358,14 @@ class CameraView(HomeAssistantView):
|
||||
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, entities):
|
||||
def __init__(self, component):
|
||||
"""Initialize a basic camera view."""
|
||||
self.entities = entities
|
||||
self.component = component
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, entity_id):
|
||||
"""Start a GET request."""
|
||||
camera = self.entities.get(entity_id)
|
||||
camera = self.component.get_entity(entity_id)
|
||||
|
||||
if camera is None:
|
||||
status = 404 if request[KEY_AUTHENTICATED] else 401
|
||||
|
||||
@@ -8,9 +8,10 @@ import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.amcrest import (
|
||||
STREAM_SOURCE_LIST, TIMEOUT)
|
||||
DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT)
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web,
|
||||
async_aiohttp_proxy_stream)
|
||||
@@ -26,21 +27,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device = discovery_info['device']
|
||||
authentication = discovery_info['authentication']
|
||||
ffmpeg_arguments = discovery_info['ffmpeg_arguments']
|
||||
name = discovery_info['name']
|
||||
resolution = discovery_info['resolution']
|
||||
stream_source = discovery_info['stream_source']
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
async_add_devices([
|
||||
AmcrestCam(hass,
|
||||
name,
|
||||
device,
|
||||
authentication,
|
||||
ffmpeg_arguments,
|
||||
stream_source,
|
||||
resolution)], True)
|
||||
async_add_devices([AmcrestCam(hass, amcrest)], True)
|
||||
|
||||
return True
|
||||
|
||||
@@ -48,18 +38,17 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, name, camera, authentication,
|
||||
ffmpeg_arguments, stream_source, resolution):
|
||||
def __init__(self, hass, amcrest):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = name
|
||||
self._camera = camera
|
||||
self._name = amcrest.name
|
||||
self._camera = amcrest.device
|
||||
self._base_url = self._camera.get_base_url()
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = ffmpeg_arguments
|
||||
self._stream_source = stream_source
|
||||
self._resolution = resolution
|
||||
self._token = self._auth = authentication
|
||||
self._ffmpeg_arguments = amcrest.ffmpeg_arguments
|
||||
self._stream_source = amcrest.stream_source
|
||||
self._resolution = amcrest.resolution
|
||||
self._token = self._auth = amcrest.authentication
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
|
||||
@@ -75,7 +75,9 @@ class ArloCam(Camera):
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._last_refresh = None
|
||||
self._camera.base_station.refresh_rate = SCAN_INTERVAL.total_seconds()
|
||||
if self._camera.base_station:
|
||||
self._camera.base_station.refresh_rate = \
|
||||
SCAN_INTERVAL.total_seconds()
|
||||
self.attrs = {}
|
||||
|
||||
def camera_image(self):
|
||||
|
||||
@@ -6,11 +6,11 @@ https://home-assistant.io/components/camera.axis/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
from homeassistant.const import (
|
||||
CONF_AUTHENTICATION, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -20,6 +20,7 @@ DEPENDENCIES = [DOMAIN]
|
||||
|
||||
|
||||
def _get_image_url(host, port, mode):
|
||||
"""Set the URL to get the image."""
|
||||
if mode == 'mjpeg':
|
||||
return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port)
|
||||
elif mode == 'single':
|
||||
@@ -27,34 +28,32 @@ def _get_image_url(host, port, mode):
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Axis camera."""
|
||||
"""Set up the Axis camera."""
|
||||
camera_config = {
|
||||
CONF_NAME: discovery_info[CONF_NAME],
|
||||
CONF_USERNAME: discovery_info[CONF_USERNAME],
|
||||
CONF_PASSWORD: discovery_info[CONF_PASSWORD],
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST],
|
||||
str(discovery_info[CONF_PORT]),
|
||||
'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info[CONF_HOST],
|
||||
str(discovery_info[CONF_PORT]),
|
||||
'single'),
|
||||
CONF_MJPEG_URL: _get_image_url(
|
||||
discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]),
|
||||
'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(
|
||||
discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]),
|
||||
'single'),
|
||||
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
|
||||
}
|
||||
add_devices([AxisCamera(hass,
|
||||
camera_config,
|
||||
str(discovery_info[CONF_PORT]))])
|
||||
add_devices([AxisCamera(
|
||||
hass, camera_config, str(discovery_info[CONF_PORT]))])
|
||||
|
||||
|
||||
class AxisCamera(MjpegCamera):
|
||||
"""AxisCamera class."""
|
||||
"""Representation of a Axis camera."""
|
||||
|
||||
def __init__(self, hass, config, port):
|
||||
"""Initialize Axis Communications camera component."""
|
||||
super().__init__(hass, config)
|
||||
self.port = port
|
||||
dispatcher_connect(hass,
|
||||
DOMAIN + '_' + config[CONF_NAME] + '_new_ip',
|
||||
self._new_ip)
|
||||
dispatcher_connect(
|
||||
hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip)
|
||||
|
||||
def _new_ip(self, host):
|
||||
"""Set new IP for video stream."""
|
||||
|
||||
@@ -4,21 +4,21 @@ Support for Blink system camera.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.blink/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
|
||||
from homeassistant.components.blink import DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a Blink Camera."""
|
||||
@@ -45,7 +45,7 @@ class BlinkCamera(Camera):
|
||||
self.notifications = self.data.cameras[self._name].notifications
|
||||
self.response = None
|
||||
|
||||
_LOGGER.info("Initialized blink camera %s", self._name)
|
||||
_LOGGER.debug("Initialized blink camera %s", self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -55,7 +55,7 @@ class BlinkCamera(Camera):
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def request_image(self):
|
||||
"""Request a new image from Blink servers."""
|
||||
_LOGGER.info("Requesting new image from blink servers")
|
||||
_LOGGER.debug("Requesting new image from blink servers")
|
||||
image_url = self.check_for_motion()
|
||||
header = self.data.cameras[self._name].header
|
||||
self.response = requests.get(image_url, headers=header, stream=True)
|
||||
@@ -68,7 +68,7 @@ class BlinkCamera(Camera):
|
||||
# We detected motion at some point
|
||||
self.data.last_motion()
|
||||
self.notifications = notifs
|
||||
# returning motion image currently not working
|
||||
# Returning motion image currently not working
|
||||
# return self.data.cameras[self._name].motion['image']
|
||||
elif notifs < self.notifications:
|
||||
self.notifications = notifs
|
||||
|
||||
95
homeassistant/components/camera/canary.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Support for Canary camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.canary/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT
|
||||
|
||||
DEPENDENCIES = ['canary']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MOTION_START_TIME = "motion_start_time"
|
||||
ATTR_MOTION_END_TIME = "motion_end_time"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Canary sensors."""
|
||||
data = hass.data[DATA_CANARY]
|
||||
devices = []
|
||||
|
||||
for location in data.locations:
|
||||
entries = data.get_motion_entries(location.location_id)
|
||||
if entries:
|
||||
devices.append(CanaryCamera(data, location.location_id,
|
||||
DEFAULT_TIMEOUT))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class CanaryCamera(Camera):
|
||||
"""An implementation of a Canary security camera."""
|
||||
|
||||
def __init__(self, data, location_id, timeout):
|
||||
"""Initialize a Canary security camera."""
|
||||
super().__init__()
|
||||
self._data = data
|
||||
self._location_id = location_id
|
||||
self._timeout = timeout
|
||||
|
||||
self._location = None
|
||||
self._motion_entry = None
|
||||
self._image_content = None
|
||||
|
||||
def camera_image(self):
|
||||
"""Update the status of the camera and return bytes of camera image."""
|
||||
self.update()
|
||||
return self._image_content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._location.name
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return self._location.is_recording
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
if self._motion_entry is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_MOTION_START_TIME: self._motion_entry.start_time,
|
||||
ATTR_MOTION_END_TIME: self._motion_entry.end_time,
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Update the status of the camera."""
|
||||
self._data.update()
|
||||
self._location = self._data.get_location(self._location_id)
|
||||
|
||||
entries = self._data.get_motion_entries(self._location_id)
|
||||
if entries:
|
||||
current = entries[0]
|
||||
previous = self._motion_entry
|
||||
|
||||
if previous is None or previous.entry_id != current.entry_id:
|
||||
self._motion_entry = current
|
||||
self._image_content = requests.get(
|
||||
current.thumbnails[0].image_url,
|
||||
timeout=self._timeout).content
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return not self._location.is_recording
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |