mirror of
https://github.com/home-assistant/core.git
synced 2026-01-04 23:05:26 +01:00
Compare commits
849 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75dd391118 | ||
|
|
76a9eba744 | ||
|
|
31fe1d28e8 | ||
|
|
a4a38c8a00 | ||
|
|
3b74cc606e | ||
|
|
b750319de4 | ||
|
|
9bc16157af | ||
|
|
35d7f2b8bb | ||
|
|
7390f82e1f | ||
|
|
cc9e5de503 | ||
|
|
50c8224365 | ||
|
|
b08b376aa7 | ||
|
|
60ef0153a2 | ||
|
|
44c4b25f2b | ||
|
|
4abcaea4b7 | ||
|
|
831cad4220 | ||
|
|
6c524594c1 | ||
|
|
78f6cfd1eb | ||
|
|
6d6abab358 | ||
|
|
326cc83a17 | ||
|
|
8358ab56ea | ||
|
|
32dc518971 | ||
|
|
b318a033bb | ||
|
|
a0b2105ea0 | ||
|
|
9f9b87692a | ||
|
|
5c4f04e9fc | ||
|
|
757f6278eb | ||
|
|
b9dcc2777b | ||
|
|
103fffa0f4 | ||
|
|
7748867732 | ||
|
|
02517ae5ec | ||
|
|
2a31bb48c6 | ||
|
|
5b70ada7b4 | ||
|
|
7b45cf8e59 | ||
|
|
394d53e748 | ||
|
|
c125c4af4f | ||
|
|
f90b89bc74 | ||
|
|
ceac9eab94 | ||
|
|
7bb0abdf09 | ||
|
|
1d60760e21 | ||
|
|
43d18daebd | ||
|
|
1a7895b1d8 | ||
|
|
c2f31bbb38 | ||
|
|
a7e75dd01e | ||
|
|
58ea3c25df | ||
|
|
6d2de67620 | ||
|
|
a359d21799 | ||
|
|
be552a59c9 | ||
|
|
832f9737a8 | ||
|
|
da6bdf275e | ||
|
|
7ca025f653 | ||
|
|
570cfc60c5 | ||
|
|
dc551b825f | ||
|
|
6da3e23436 | ||
|
|
e4b6395250 | ||
|
|
72bd9fb5c7 | ||
|
|
2dec38d8d4 | ||
|
|
acb841a1f4 | ||
|
|
eeb8bc3913 | ||
|
|
12f790c7cf | ||
|
|
dbb4e4c3fa | ||
|
|
d51e62d0a3 | ||
|
|
ab92a91ac5 | ||
|
|
cfa36f3546 | ||
|
|
96d8fbe513 | ||
|
|
1e9d91be0e | ||
|
|
2402897f47 | ||
|
|
b857d5dad0 | ||
|
|
d17753009a | ||
|
|
3467020dbf | ||
|
|
4114884cdc | ||
|
|
d7ccf07922 | ||
|
|
2a7fa5afc3 | ||
|
|
04aa4e898a | ||
|
|
b156ae7812 | ||
|
|
48928d1f9e | ||
|
|
df98d5b3c1 | ||
|
|
f4b5c439a1 | ||
|
|
ecc514b7e4 | ||
|
|
6edb54052f | ||
|
|
4d2480bbd1 | ||
|
|
2708e193ec | ||
|
|
c3923b2768 | ||
|
|
080c4efb00 | ||
|
|
99f1ea9b59 | ||
|
|
46cad514d4 | ||
|
|
e0552ad899 | ||
|
|
5c99dd0e3d | ||
|
|
cdf9464698 | ||
|
|
7ba25f3526 | ||
|
|
ee5b9e7291 | ||
|
|
167260bcc6 | ||
|
|
64de1c9777 | ||
|
|
1547045f2c | ||
|
|
d02899216d | ||
|
|
0aac4d64e1 | ||
|
|
0bf9e6d4bb | ||
|
|
f78246e686 | ||
|
|
c90a1b9760 | ||
|
|
14446c5731 | ||
|
|
2e2b764dbe | ||
|
|
695f062e29 | ||
|
|
194b268ae3 | ||
|
|
8295fc8b4c | ||
|
|
d0dcd1bb73 | ||
|
|
82ad8b0a8f | ||
|
|
91a9da8f0c | ||
|
|
e3415c4e22 | ||
|
|
9bca3f3103 | ||
|
|
7c3ae884df | ||
|
|
8a4aace789 | ||
|
|
0e74cd833d | ||
|
|
5e2911f071 | ||
|
|
7dacc4a7bb | ||
|
|
98fe50d5ad | ||
|
|
37e3c2a133 | ||
|
|
76b79019ce | ||
|
|
c40ddf18c7 | ||
|
|
9a3fe691b1 | ||
|
|
8826e6a8d0 | ||
|
|
860a12cffb | ||
|
|
76ff934bd3 | ||
|
|
d968e1d011 | ||
|
|
fa0dbaf065 | ||
|
|
4d0f19496a | ||
|
|
0cc9555d14 | ||
|
|
d712a3dc38 | ||
|
|
84446bed14 | ||
|
|
e92b15f966 | ||
|
|
a458ce8069 | ||
|
|
5e492db9a3 | ||
|
|
bc646070c8 | ||
|
|
64290d74f0 | ||
|
|
a11b68c560 | ||
|
|
8ca2345fd4 | ||
|
|
8c628071f3 | ||
|
|
81d38c3463 | ||
|
|
776455030f | ||
|
|
8afd30b7d4 | ||
|
|
b60f5714fc | ||
|
|
fa8bc0a36c | ||
|
|
1ae8256ffd | ||
|
|
b3253403aa | ||
|
|
308744d8a0 | ||
|
|
13006cee68 | ||
|
|
e21382cd3e | ||
|
|
71fc446425 | ||
|
|
03d19ec2f1 | ||
|
|
5a7e446646 | ||
|
|
2b3caa716a | ||
|
|
6574dd8439 | ||
|
|
6d56519297 | ||
|
|
60bcb12a48 | ||
|
|
58509f8bba | ||
|
|
2099d023ef | ||
|
|
db6a6fa4cb | ||
|
|
d89bfcdaa5 | ||
|
|
64b1179c13 | ||
|
|
840e27adec | ||
|
|
31a8537ab5 | ||
|
|
87dab37b8a | ||
|
|
3a2cdd3de0 | ||
|
|
2009e98497 | ||
|
|
b354a18bf3 | ||
|
|
1cd3cd8d77 | ||
|
|
d9556392bc | ||
|
|
695fb412cd | ||
|
|
93322b0251 | ||
|
|
9b9b625ac4 | ||
|
|
0ae6585a90 | ||
|
|
cffc7ac4d8 | ||
|
|
a9be6c36f1 | ||
|
|
1b35f0878e | ||
|
|
93872590b6 | ||
|
|
b2a15e17d3 | ||
|
|
9bf13231f7 | ||
|
|
c8c6bee539 | ||
|
|
b5c2be8ffa | ||
|
|
4d35f2805f | ||
|
|
53c1b93b61 | ||
|
|
c25aa56751 | ||
|
|
e8c9dcf0fe | ||
|
|
ca63e44227 | ||
|
|
776e53a7f0 | ||
|
|
a099430834 | ||
|
|
7746ecd98e | ||
|
|
10d1496f5a | ||
|
|
cf0ff54d14 | ||
|
|
97cc76b43e | ||
|
|
c89e6ec915 | ||
|
|
efdf51b542 | ||
|
|
bbb251c0cf | ||
|
|
94b719e150 | ||
|
|
69d3a3dd32 | ||
|
|
4904653b70 | ||
|
|
dddf4d1460 | ||
|
|
9a6c9cff30 | ||
|
|
d3b62e1fe1 | ||
|
|
f63a79ee8f | ||
|
|
898ba56d9f | ||
|
|
64a5bff5b2 | ||
|
|
af7de8d5ae | ||
|
|
754d98bcd5 | ||
|
|
4874030b70 | ||
|
|
84c89686a9 | ||
|
|
48fd8f1f63 | ||
|
|
83a108b20a | ||
|
|
b0a800cc6d | ||
|
|
1f5f4e7a89 | ||
|
|
b1fbada02d | ||
|
|
08909ed420 | ||
|
|
ec8969351d | ||
|
|
801a69be3a | ||
|
|
51e20c92f9 | ||
|
|
443553ff16 | ||
|
|
2e6a48ff5f | ||
|
|
49cfe38cca | ||
|
|
8a042586f1 | ||
|
|
08f8e540e3 | ||
|
|
f09b888a8a | ||
|
|
279f82acc4 | ||
|
|
de6c5a503b | ||
|
|
898f89ffc7 | ||
|
|
5c807c6bd9 | ||
|
|
dd84b4e237 | ||
|
|
6dfae7a259 | ||
|
|
c6c8cd4f51 | ||
|
|
bde7176b3c | ||
|
|
4c03d670c1 | ||
|
|
406afbb369 | ||
|
|
9c6609cb79 | ||
|
|
e5504b39ec | ||
|
|
b1ef5042f9 | ||
|
|
b35fa4f1c1 | ||
|
|
71da9d2f50 | ||
|
|
86388f5af2 | ||
|
|
17f0fb69bd | ||
|
|
2d02baf3d0 | ||
|
|
66473120ab | ||
|
|
6ddbb4d568 | ||
|
|
154c69a454 | ||
|
|
ad4ec49f9c | ||
|
|
e8367f245a | ||
|
|
4bc37bd661 | ||
|
|
b4841a17a6 | ||
|
|
3b9d5cdf73 | ||
|
|
77d568dc47 | ||
|
|
9db1ff8cd4 | ||
|
|
248a90b71d | ||
|
|
d8c4af9c81 | ||
|
|
1e6c660f59 | ||
|
|
44a508e86c | ||
|
|
92c6cee2a1 | ||
|
|
d4bc8e23af | ||
|
|
f0db698f75 | ||
|
|
cf57db919e | ||
|
|
84b12ab007 | ||
|
|
601193b1d2 | ||
|
|
038b1c1fc6 | ||
|
|
0d734303a4 | ||
|
|
ff4cb23f2a | ||
|
|
e94b4ec006 | ||
|
|
be91207830 | ||
|
|
ecf285105c | ||
|
|
767f3d58ff | ||
|
|
34097cda24 | ||
|
|
0ce3703e30 | ||
|
|
464e843186 | ||
|
|
5d2b7a6e0b | ||
|
|
914a868fbd | ||
|
|
03e0c7c71c | ||
|
|
32ffd006fa | ||
|
|
58b85b2e0e | ||
|
|
61653a517d | ||
|
|
2a7bc0e55c | ||
|
|
95b439fbd5 | ||
|
|
1872481f47 | ||
|
|
44b6d23e0f | ||
|
|
58eb0ec52a | ||
|
|
febe16d700 | ||
|
|
8c56091af7 | ||
|
|
eacdce9ed9 | ||
|
|
42c99b0ccb | ||
|
|
2a6c0cfc17 | ||
|
|
84040892df | ||
|
|
345008c673 | ||
|
|
14d1494cd2 | ||
|
|
f1d11e77ed | ||
|
|
b1b8715f7d | ||
|
|
b6d559da1f | ||
|
|
475c412ae4 | ||
|
|
c04a002c55 | ||
|
|
5013a82655 | ||
|
|
05181bf232 | ||
|
|
c22a73e1d0 | ||
|
|
c9b353f7a7 | ||
|
|
64cfc4ff02 | ||
|
|
bb46009efa | ||
|
|
3f9250415f | ||
|
|
c294a534d0 | ||
|
|
85d6970df8 | ||
|
|
260a619a40 | ||
|
|
0c6ef3b7f9 | ||
|
|
0c47434aad | ||
|
|
1d8a1df2c4 | ||
|
|
65b85ec6c0 | ||
|
|
b6b9da7e6e | ||
|
|
d18f2684fb | ||
|
|
e93b079ef4 | ||
|
|
356ad6e468 | ||
|
|
8f35212dd6 | ||
|
|
0827a26642 | ||
|
|
b4756e6dda | ||
|
|
4cc192e445 | ||
|
|
962e5315ab | ||
|
|
2c7e895105 | ||
|
|
00019b9ff0 | ||
|
|
8e776b4dc0 | ||
|
|
ce13b0989d | ||
|
|
c81735cc84 | ||
|
|
5d18759146 | ||
|
|
9cdcfae8f3 | ||
|
|
547d93f631 | ||
|
|
d841ddc50b | ||
|
|
40b5824230 | ||
|
|
86f3e2455d | ||
|
|
9a065cc536 | ||
|
|
8e4dbcaf21 | ||
|
|
6863d2e0af | ||
|
|
c23809488b | ||
|
|
248f5c0209 | ||
|
|
e5aa40fa5d | ||
|
|
1f573b46a4 | ||
|
|
0647bb7f6b | ||
|
|
a73fbbaf7a | ||
|
|
755f5b61b7 | ||
|
|
6869c7401e | ||
|
|
835577b2bc | ||
|
|
859d0d5ad6 | ||
|
|
aed797f438 | ||
|
|
eb8093934f | ||
|
|
608b482906 | ||
|
|
7207c2cca1 | ||
|
|
ed1d0b4197 | ||
|
|
63461e9007 | ||
|
|
40a2145558 | ||
|
|
d883b18751 | ||
|
|
b8e462cf5b | ||
|
|
11df7becd3 | ||
|
|
99f5db8c02 | ||
|
|
19b08a975a | ||
|
|
123f4acfc1 | ||
|
|
0f90426023 | ||
|
|
8b6a94b0f5 | ||
|
|
0a333230c1 | ||
|
|
455e1df7cb | ||
|
|
f71396c293 | ||
|
|
d930c399fe | ||
|
|
f3748ce535 | ||
|
|
8beefcfc69 | ||
|
|
93747f2766 | ||
|
|
7af438fa2f | ||
|
|
2b5fcd737b | ||
|
|
2b320f23fc | ||
|
|
679d500e61 | ||
|
|
613615433a | ||
|
|
f70ff66d11 | ||
|
|
d2bbc6ef70 | ||
|
|
37e28428c1 | ||
|
|
c56f99baaf | ||
|
|
265232af98 | ||
|
|
e6c4113c5b | ||
|
|
c86e1b31b3 | ||
|
|
5912316496 | ||
|
|
58f0655298 | ||
|
|
43a93fb345 | ||
|
|
36b338051b | ||
|
|
fc566309c1 | ||
|
|
23ce9949b1 | ||
|
|
275c80183c | ||
|
|
cd1655f43b | ||
|
|
1a117d0bea | ||
|
|
944bb8474f | ||
|
|
779f520c56 | ||
|
|
82ed7b6b08 | ||
|
|
af77341494 | ||
|
|
23fb8c4cdd | ||
|
|
726bc5b670 | ||
|
|
b615b3349f | ||
|
|
0f59bb208c | ||
|
|
38d201a54a | ||
|
|
c8bc1e3c5d | ||
|
|
a862bc4edc | ||
|
|
b0e3d5a576 | ||
|
|
f006b00dc1 | ||
|
|
1fff6ce438 | ||
|
|
c06c82905a | ||
|
|
2b86d89bb4 | ||
|
|
7bdb79bd54 | ||
|
|
41aaeb715a | ||
|
|
5d8a465c18 | ||
|
|
c6f5a5443f | ||
|
|
d6cb102f63 | ||
|
|
edde76e544 | ||
|
|
d5fff2f94a | ||
|
|
0e0ba28249 | ||
|
|
6745e83a6c | ||
|
|
44bc057fdb | ||
|
|
96b8d8fcfa | ||
|
|
fc2df34206 | ||
|
|
09c29737de | ||
|
|
4c01b47945 | ||
|
|
7aaf3a46db | ||
|
|
d774ba46c7 | ||
|
|
4c37ee8884 | ||
|
|
7f5f458074 | ||
|
|
479457d6ec | ||
|
|
7e73d27dd1 | ||
|
|
e7ffec87ac | ||
|
|
2d47b187c5 | ||
|
|
fe2103dedb | ||
|
|
7bf5d1c662 | ||
|
|
cb24282040 | ||
|
|
bd9429d3af | ||
|
|
d7a005ad0f | ||
|
|
2e2a996a8e | ||
|
|
0364498dee | ||
|
|
c5fdd4392a | ||
|
|
895454b6c3 | ||
|
|
2109b7a1b9 | ||
|
|
71a305ea45 | ||
|
|
e73634e6c7 | ||
|
|
3d47ad5018 | ||
|
|
c823ea9f2a | ||
|
|
75bcb1ff0f | ||
|
|
1663cc9084 | ||
|
|
17cfcc981d | ||
|
|
60fabaec24 | ||
|
|
5e44934e7e | ||
|
|
01a6c1c1c8 | ||
|
|
cd1b0ac67d | ||
|
|
2bfded7153 | ||
|
|
20af5cb5b4 | ||
|
|
080f56e0f5 | ||
|
|
173e15e733 | ||
|
|
72407c2f95 | ||
|
|
1b79722b69 | ||
|
|
cc5233103c | ||
|
|
2feea1d1eb | ||
|
|
2c39c39d52 | ||
|
|
6e6b1ef7ab | ||
|
|
55ddaf1ee7 | ||
|
|
6860d9b096 | ||
|
|
3e1cc4282e | ||
|
|
200bdb30ff | ||
|
|
eb17ba970c | ||
|
|
ffe4c425af | ||
|
|
a18fdbfbb8 | ||
|
|
58600f25b3 | ||
|
|
749fc583ea | ||
|
|
b07d887d77 | ||
|
|
9bb94a4512 | ||
|
|
e76d553513 | ||
|
|
844799a1f7 | ||
|
|
e005ebe989 | ||
|
|
e9d19c1dcc | ||
|
|
7d2ab4fce6 | ||
|
|
ba2ea35089 | ||
|
|
ade62faa38 | ||
|
|
ee322dbbdc | ||
|
|
0d4141bf13 | ||
|
|
d404ac8978 | ||
|
|
71da21dcc8 | ||
|
|
04dbc992ec | ||
|
|
6d0e08cf7d | ||
|
|
1e0025acae | ||
|
|
8fc853ba11 | ||
|
|
8cbb8f6527 | ||
|
|
4f86c9ecda | ||
|
|
9561fed650 | ||
|
|
67b599475e | ||
|
|
114ece1848 | ||
|
|
c05815cced | ||
|
|
2e0c185740 | ||
|
|
231ef40f53 | ||
|
|
b4159c7dc9 | ||
|
|
8cc5fc1369 | ||
|
|
fc3235fb6d | ||
|
|
d129df93dd | ||
|
|
67336a111b | ||
|
|
0af1a96f14 | ||
|
|
272899ec96 | ||
|
|
7d28d9d6b4 | ||
|
|
6a92e27e2f | ||
|
|
faceb4c1dc | ||
|
|
6d5f00098a | ||
|
|
618a86a37c | ||
|
|
880ef8af48 | ||
|
|
95124c7ddb | ||
|
|
0aba227300 | ||
|
|
734bd75fd3 | ||
|
|
0c5e077091 | ||
|
|
1ed2f8ae91 | ||
|
|
a343c20404 | ||
|
|
d4e8b831a0 | ||
|
|
98f41d6b84 | ||
|
|
7774a03a55 | ||
|
|
c35e5c9997 | ||
|
|
5d862e426e | ||
|
|
bab8d574fe | ||
|
|
c980d26aae | ||
|
|
08f75f7935 | ||
|
|
1ad14b8227 | ||
|
|
382ac5c3b5 | ||
|
|
af297aa0dc | ||
|
|
20e1b3eae0 | ||
|
|
28861221ae | ||
|
|
f367c49fb9 | ||
|
|
ad8645baf4 | ||
|
|
62785c2431 | ||
|
|
22c3d014aa | ||
|
|
3f3127a290 | ||
|
|
88fc64c8a0 | ||
|
|
1463fc4fe0 | ||
|
|
ece58ce78f | ||
|
|
b67f1fed52 | ||
|
|
4770888d22 | ||
|
|
1d0f3b930f | ||
|
|
22e2262f8e | ||
|
|
53d1a040d4 | ||
|
|
d7d71c97e2 | ||
|
|
c15fd4323e | ||
|
|
91227d9a2e | ||
|
|
a3db0ec231 | ||
|
|
18e965c3cd | ||
|
|
4cc417677e | ||
|
|
525d735f21 | ||
|
|
e88b98f5fa | ||
|
|
6f68752d1e | ||
|
|
61a0976752 | ||
|
|
d7b3c9c38e | ||
|
|
a01939c6e9 | ||
|
|
c128919b5f | ||
|
|
e5d69feb93 | ||
|
|
ee5f228309 | ||
|
|
15dde7925a | ||
|
|
fcf318cf53 | ||
|
|
c2a5f63b1f | ||
|
|
79fa2d4175 | ||
|
|
214a18f08c | ||
|
|
ded2ea8b19 | ||
|
|
1d100dcac9 | ||
|
|
a3ae96440b | ||
|
|
0235626f40 | ||
|
|
d7dd7df5e7 | ||
|
|
1e28851280 | ||
|
|
df68de8032 | ||
|
|
f3595f790a | ||
|
|
0d14920758 | ||
|
|
2940fb72fb | ||
|
|
8e0838adeb | ||
|
|
4e820ea30a | ||
|
|
26490109ac | ||
|
|
e4a713207d | ||
|
|
cc0d0a38d7 | ||
|
|
afde5a6b26 | ||
|
|
a5fb284717 | ||
|
|
e487a09190 | ||
|
|
52eb816c62 | ||
|
|
90d894a499 | ||
|
|
ba13951fff | ||
|
|
2a7b7ebd6a | ||
|
|
df7d9c3bb2 | ||
|
|
c549ea115d | ||
|
|
dad54bb993 | ||
|
|
0211cf29eb | ||
|
|
1d9ac5f8b3 | ||
|
|
7f699b4261 | ||
|
|
a1e910f1cf | ||
|
|
b4899ec469 | ||
|
|
06de7053ce | ||
|
|
4484a7a94b | ||
|
|
a89e635bf3 | ||
|
|
3ab056ba69 | ||
|
|
5ba815ab21 | ||
|
|
274e9799b3 | ||
|
|
5ce9aea65d | ||
|
|
705814cb08 | ||
|
|
8e695d1eb0 | ||
|
|
be272ac64a | ||
|
|
b910a9917d | ||
|
|
9649097b32 | ||
|
|
5e76a51db4 | ||
|
|
27abac85b6 | ||
|
|
9f2aae1357 | ||
|
|
e6ece4bf6d | ||
|
|
aea2d1b317 | ||
|
|
33e46b484f | ||
|
|
3f6a5564ad | ||
|
|
3317b4916b | ||
|
|
9c0455e3dc | ||
|
|
5d43d3eb1c | ||
|
|
4163e55dbd | ||
|
|
3cc4fdaa34 | ||
|
|
edeb31d74e | ||
|
|
54d19e3c53 | ||
|
|
892f455aee | ||
|
|
942d630762 | ||
|
|
9ea1101aba | ||
|
|
08a65a3b31 | ||
|
|
d4b3f56d53 | ||
|
|
5a2b4a5376 | ||
|
|
9d836a115a | ||
|
|
bf92aedd38 | ||
|
|
230c3815f2 | ||
|
|
9afe066ec8 | ||
|
|
66541a6a19 | ||
|
|
825ee3612d | ||
|
|
65bd7d2326 | ||
|
|
d8c1013b09 | ||
|
|
02d1dc6247 | ||
|
|
726d950522 | ||
|
|
3324995e70 | ||
|
|
85747fe2ef | ||
|
|
09db875ace | ||
|
|
7d407756c3 | ||
|
|
91d682d02c | ||
|
|
b75c103db4 | ||
|
|
bba323d226 | ||
|
|
7564d539c1 | ||
|
|
33439aaa22 | ||
|
|
d5368f6f78 | ||
|
|
d9999f36e8 | ||
|
|
235e1a0885 | ||
|
|
541fec0534 | ||
|
|
b3ad7989ae | ||
|
|
3d897e0e52 | ||
|
|
c6d5987109 | ||
|
|
5d3956ea98 | ||
|
|
4fb0b27310 | ||
|
|
4833e992fb | ||
|
|
fe3aed0f0c | ||
|
|
57402bcb43 | ||
|
|
7f48c00793 | ||
|
|
f58647849a | ||
|
|
1f468fc94d | ||
|
|
961c02f72a | ||
|
|
79da1ec0d9 | ||
|
|
fe174402d2 | ||
|
|
1b2dfb8ed1 | ||
|
|
297a6f6f03 | ||
|
|
0dfcf40d37 | ||
|
|
d308ea69ce | ||
|
|
a8c5c995a0 | ||
|
|
86b318e992 | ||
|
|
53ea926292 | ||
|
|
2b1f4123db | ||
|
|
b36e346ccb | ||
|
|
89e8fb4066 | ||
|
|
e2d23d902a | ||
|
|
2604dd89a6 | ||
|
|
044b9caa76 | ||
|
|
1707cdf9f3 | ||
|
|
4c86721e70 | ||
|
|
0ff500ca25 | ||
|
|
23f54b07c7 | ||
|
|
f25ddef4d7 | ||
|
|
7158919346 | ||
|
|
627517cbbc | ||
|
|
4ecfc7d066 | ||
|
|
72751b95b5 | ||
|
|
fc3b7907ed | ||
|
|
f26a7fc6bb | ||
|
|
0c563f7b14 | ||
|
|
519d9f2fd0 | ||
|
|
3701ac292c | ||
|
|
1db18478d2 | ||
|
|
3230869f74 | ||
|
|
9aa88819a5 | ||
|
|
3e92318cb2 | ||
|
|
f0a38dded6 | ||
|
|
c32f47aea6 | ||
|
|
626763a7c3 | ||
|
|
5df8477536 | ||
|
|
1f89e6ddba | ||
|
|
13ab2be5f6 | ||
|
|
2bc84af87e | ||
|
|
ef2ed7bfc9 | ||
|
|
6040a40af2 | ||
|
|
fb352c20d9 | ||
|
|
678f30def1 | ||
|
|
0fce5ccc7f | ||
|
|
02afc98668 | ||
|
|
57777ef79a | ||
|
|
ca6fa1313e | ||
|
|
ea91d24eb2 | ||
|
|
6e5a3c0a94 | ||
|
|
754d536974 | ||
|
|
4f6ed09a99 | ||
|
|
8d375e2d47 | ||
|
|
1d2d338cd0 | ||
|
|
cb47507002 | ||
|
|
fae620f3b3 | ||
|
|
b821a82417 | ||
|
|
da7837af73 | ||
|
|
6e903fd429 | ||
|
|
9f7e167669 | ||
|
|
54a64fb8d9 | ||
|
|
2d89c3ecf4 | ||
|
|
9f6d1c4e7b | ||
|
|
62b8e54235 | ||
|
|
1ceac8407d | ||
|
|
0c0b02eb3d | ||
|
|
c70722dbae | ||
|
|
a05fb4cef8 | ||
|
|
fee01fcccc | ||
|
|
2b37b4251b | ||
|
|
3aa1b6a3f8 | ||
|
|
c32afcd961 | ||
|
|
d60c2d604f | ||
|
|
081e61528d | ||
|
|
5799d1aec9 | ||
|
|
7d32e5eeeb | ||
|
|
57f32fa629 | ||
|
|
7da47852d4 | ||
|
|
f1b658ea5d | ||
|
|
754e93ff6a | ||
|
|
947c1efca2 | ||
|
|
a1239077d9 | ||
|
|
53b5dc8e84 | ||
|
|
09bcd7321a | ||
|
|
14ef0ca786 | ||
|
|
272539105f | ||
|
|
7d67017de7 | ||
|
|
d921073e77 | ||
|
|
9cf2acb495 | ||
|
|
3b424b034a | ||
|
|
76598bc4d2 | ||
|
|
c54476b62f | ||
|
|
ae8a8e22ad | ||
|
|
4c8d1d9d2f | ||
|
|
daea93d9f9 | ||
|
|
1540bb1279 | ||
|
|
555e533f67 | ||
|
|
118f2f0bad | ||
|
|
c8add59ea5 | ||
|
|
18f5258aaf | ||
|
|
207c9e8575 | ||
|
|
4891ca1610 | ||
|
|
10c9132046 | ||
|
|
71ee847aee | ||
|
|
d9ae7ceb0c | ||
|
|
2a972b2334 | ||
|
|
7484152be1 | ||
|
|
6581dc2381 | ||
|
|
31ec0ac6a7 | ||
|
|
8b2edc1514 | ||
|
|
2612c6d6b8 | ||
|
|
0b8b9ecb94 | ||
|
|
a0fdb2778d | ||
|
|
5ef8ca9b03 | ||
|
|
a10fa90357 | ||
|
|
9743e17d62 | ||
|
|
6fcb1b548e | ||
|
|
1bf5554017 | ||
|
|
49b1643ff0 | ||
|
|
ce19e6367f | ||
|
|
180e146e14 | ||
|
|
bead274b20 | ||
|
|
1cbf8c8049 | ||
|
|
ad259ead50 | ||
|
|
bb457f47cc | ||
|
|
df3e904fe7 | ||
|
|
7697cdef0a | ||
|
|
6951b6f60b | ||
|
|
6ca0d4cd14 | ||
|
|
a5b756e1e5 | ||
|
|
7848791a04 | ||
|
|
f916fc04f9 | ||
|
|
8f4608c654 | ||
|
|
399a0b470a | ||
|
|
4d716cec2b | ||
|
|
1373db8b60 | ||
|
|
7771cc2ccf | ||
|
|
c5ad7996fb | ||
|
|
3d94f77998 | ||
|
|
6330d9cde6 | ||
|
|
9a0bb62654 | ||
|
|
d873a7baf0 | ||
|
|
c663d85129 | ||
|
|
8c13d3ed4c | ||
|
|
cb322f72db | ||
|
|
39a446c43c | ||
|
|
aa8622f8e8 | ||
|
|
e031b8078f | ||
|
|
e1647fb6ac | ||
|
|
10feac11d9 | ||
|
|
d4e2332ce0 | ||
|
|
1d7169403b | ||
|
|
e4685de459 | ||
|
|
d83de36c32 | ||
|
|
73547c8c4b | ||
|
|
40094cecae | ||
|
|
0b327cd4d9 | ||
|
|
d302dbec2d | ||
|
|
e135691bd6 | ||
|
|
d4dc2707a1 | ||
|
|
a8cdf36d5c | ||
|
|
a99f36f519 | ||
|
|
0568ef025b | ||
|
|
8ded8f572a | ||
|
|
7cf2c48175 | ||
|
|
7cf9ff83bc | ||
|
|
dfe9af7110 | ||
|
|
016e8f833d | ||
|
|
574df0f420 | ||
|
|
f18f181962 | ||
|
|
4754455295 | ||
|
|
8c1317f278 | ||
|
|
7c2cb6cffd | ||
|
|
8c9d1d9af1 | ||
|
|
941fccd3fc | ||
|
|
711526e574 | ||
|
|
a2503e4d13 | ||
|
|
656ee52435 | ||
|
|
002660fd9e | ||
|
|
63580f9e03 | ||
|
|
87d9cdd78f | ||
|
|
c8ca66b671 | ||
|
|
9b98d470c2 | ||
|
|
b19ec21e88 | ||
|
|
3b331eac56 | ||
|
|
552265bc31 | ||
|
|
df58f718ab | ||
|
|
76a1a54369 | ||
|
|
b6e008be71 | ||
|
|
9d5c20b629 | ||
|
|
63d9ea6643 | ||
|
|
1d0df63615 | ||
|
|
154eacef6c | ||
|
|
dc95b28487 | ||
|
|
4d9bac6f9c | ||
|
|
6419d273ea | ||
|
|
7882b19dc5 | ||
|
|
8f8bba4ad7 | ||
|
|
7b40a641ec | ||
|
|
1b26b5ad14 | ||
|
|
bbbb4441ea | ||
|
|
1e2e877302 |
79
.coveragerc
79
.coveragerc
@@ -28,9 +28,15 @@ omit =
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
homeassistant/components/*/ios.py
|
||||
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
@@ -92,34 +98,47 @@ omit =
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/ffmpeg.py
|
||||
homeassistant/components/*/ffmpeg.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/mochad.py
|
||||
homeassistant/components/*/mochad.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
homeassistant/components/binary_sensor/flic.py
|
||||
homeassistant/components/binary_sensor/hikvision.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
@@ -127,16 +146,19 @@ omit =
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bbox.py
|
||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.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/tplink.py
|
||||
@@ -144,11 +166,11 @@ omit =
|
||||
homeassistant/components/device_tracker/volvooncall.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/garage_door/rpi_gpio.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/joaoapps_join.py
|
||||
@@ -162,12 +184,17 @@ omit =
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/denonavr.py
|
||||
homeassistant/components/media_player/directv.py
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
@@ -178,6 +205,7 @@ omit =
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pandora.py
|
||||
homeassistant/components/media_player/philips_js.py
|
||||
homeassistant/components/media_player/pioneer.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/roku.py
|
||||
@@ -186,6 +214,7 @@ omit =
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
@@ -197,7 +226,9 @@ omit =
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
homeassistant/components/notify/pushetta.py
|
||||
@@ -209,20 +240,28 @@ omit =
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/telstra.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/openalpr.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
@@ -235,16 +274,21 @@ omit =
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hddtemp.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/mhz19.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/ohmconnect.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
@@ -252,15 +296,19 @@ omit =
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
@@ -270,31 +318,32 @@ omit =
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yahoo_finance.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/broadlink.py
|
||||
homeassistant/components/switch/digitalloggers.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pilight.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/thermostat/eq3btsmart.py
|
||||
homeassistant/components/thermostat/heatmiser.py
|
||||
homeassistant/components/thermostat/homematic.py
|
||||
homeassistant/components/thermostat/proliphix.py
|
||||
homeassistant/components/thermostat/radiotherm.py
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/zeroconf.py
|
||||
|
||||
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,9 +1,9 @@
|
||||
**Description:**
|
||||
|
||||
|
||||
**Related issue (if applicable):** fixes #
|
||||
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
|
||||
|
||||
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
|
||||
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
|
||||
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
```yaml
|
||||
@@ -13,7 +13,7 @@
|
||||
**Checklist:**
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
|
||||
If the code communicates with devices, web services, or third-party tools:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -62,6 +62,7 @@ pip-log.txt
|
||||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
htmlcov/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
2
.hound.yml
Normal file
2
.hound.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
python:
|
||||
enabled: true
|
||||
@@ -2,11 +2,11 @@ sudo: false
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.4"
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=py34
|
||||
- python: "3.4"
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
- python: "3.5"
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=lint
|
||||
- python: "3.5"
|
||||
env: TOXENV=typing
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# Contributing to Home Assistant
|
||||
|
||||
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
|
||||
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/faster_reviews.md) by Kubernetes (but skip step 0)
|
||||
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
|
||||
- Write the code for your device, notification service, sensor, or IoT thing.
|
||||
- Ensure tests work.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
||||
|
||||
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
|
||||
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
|
||||
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -8,9 +8,12 @@ WORKDIR /usr/src/app
|
||||
|
||||
RUN pip3 install --no-cache-dir colorlog cython
|
||||
|
||||
# For the nmap tracker, bluetooth tracker, Z-Wave
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev && \
|
||||
# For the nmap tracker, bluetooth tracker, Z-Wave, tellstick
|
||||
RUN echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list && \
|
||||
wget -qO - http://download.telldus.se/debian/telldus-public.key | apt-key add - && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev \
|
||||
libtelldus-core2 && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY script/build_python_openzwave script/build_python_openzwave
|
||||
@@ -19,8 +22,7 @@ RUN script/build_python_openzwave && \
|
||||
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
|
||||
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
# certifi breaks Debian based installs
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install mysqlclient psycopg2 uvloop
|
||||
|
||||
# Copy source
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
homeassistant:
|
||||
# Omitted values in this section will be auto detected using freegeoip.io
|
||||
|
||||
# Location required to calculate the time the sun rises and sets.
|
||||
# Location required to calculate the time the sun rises and sets.
|
||||
# Coordinates are also used for location for weather related components.
|
||||
# Google Maps can be used to determine more precise GPS coordinates.
|
||||
latitude: 32.87336
|
||||
@@ -43,12 +43,10 @@ device_tracker:
|
||||
username: admin
|
||||
password: PASSWORD
|
||||
|
||||
chromecast:
|
||||
|
||||
switch:
|
||||
platform: wemo
|
||||
|
||||
thermostat:
|
||||
climate:
|
||||
platform: nest
|
||||
# Required: username and password that are used to login to the Nest thermostat.
|
||||
username: myemail@mydomain.com
|
||||
@@ -79,7 +77,6 @@ group:
|
||||
entities:
|
||||
- group.awesome_people
|
||||
- group.climate
|
||||
|
||||
kitchen:
|
||||
name: Kitchen
|
||||
entities:
|
||||
@@ -92,52 +89,23 @@ group:
|
||||
- input_boolean.notify_home
|
||||
- camera.demo_camera
|
||||
|
||||
example:
|
||||
|
||||
simple_alarm:
|
||||
# Which light/light group has to flash when a known device comes home
|
||||
known_light: light.Bowl
|
||||
# Which light/light group has to flash red when light turns on while no one home
|
||||
unknown_light: group.living_room
|
||||
|
||||
browser:
|
||||
|
||||
keyboard:
|
||||
|
||||
# https://home-assistant.io/getting-started/automation/
|
||||
automation:
|
||||
- alias: 'Rule 1 Light on in the evening'
|
||||
trigger:
|
||||
- platform: sun
|
||||
- alias: Turn on light when sun sets
|
||||
trigger:
|
||||
platform: sun
|
||||
event: sunset
|
||||
offset: "-01:00:00"
|
||||
- platform: state
|
||||
condition:
|
||||
condition: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
condition:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: home
|
||||
- platform: time
|
||||
after: "16:00:00"
|
||||
before: "23:00:00"
|
||||
action:
|
||||
service: homeassistant.turn_on
|
||||
entity_id: group.living_room
|
||||
state: 'home'
|
||||
action:
|
||||
service: light.turn_on
|
||||
|
||||
- alias: 'Rule 2 - Away Mode'
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
state: 'not_home'
|
||||
|
||||
condition: use_trigger_values
|
||||
action:
|
||||
service: light.turn_off
|
||||
entity_id: group.all_lights
|
||||
|
||||
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
|
||||
# Each sensor label should be unique or your sensors might not load correctly.
|
||||
# Another way to do is to collect all entries under one "sensor:"
|
||||
# sensor:
|
||||
# - platform: mqtt
|
||||
@@ -154,34 +122,30 @@ sensor:
|
||||
arg: '/'
|
||||
- type: 'disk_use_percent'
|
||||
arg: '/home'
|
||||
- type: 'disk_use'
|
||||
arg: '/home'
|
||||
|
||||
sensor 2:
|
||||
platform: forecast
|
||||
api_key: <register on Forecast.io for your PRIVATE API>
|
||||
monitored_conditions:
|
||||
- summary
|
||||
- precip_type
|
||||
- precip_intensity
|
||||
- temperature
|
||||
platform: cpuspeed
|
||||
|
||||
script:
|
||||
# Turns on the bedroom lights and then the living room lights 1 minute later
|
||||
wakeup:
|
||||
alias: Wake Up
|
||||
sequence:
|
||||
# alias is optional
|
||||
- event: LOGBOOK_ENTRY
|
||||
event_data:
|
||||
name: Paulus
|
||||
message: is waking up
|
||||
entity_id: device_tracker.paulus
|
||||
domain: light
|
||||
- alias: Bedroom lights on
|
||||
execute_service: light.turn_on
|
||||
service_data:
|
||||
service: light.turn_on
|
||||
data:
|
||||
entity_id: group.bedroom
|
||||
brightness: 100
|
||||
- delay:
|
||||
# supports seconds, milliseconds, minutes, hours, etc.
|
||||
minutes: 1
|
||||
- alias: Living room lights on
|
||||
execute_service: light.turn_on
|
||||
service_data:
|
||||
service: light.turn_on
|
||||
data:
|
||||
entity_id: group.living_room
|
||||
|
||||
scene:
|
||||
|
||||
@@ -8,11 +8,31 @@
|
||||
.. autoclass:: Config
|
||||
:members:
|
||||
|
||||
.. autoclass:: Event
|
||||
:members:
|
||||
|
||||
.. autoclass:: EventBus
|
||||
:members:
|
||||
|
||||
.. autoclass:: HomeAssistant
|
||||
:members:
|
||||
|
||||
.. autoclass:: State
|
||||
:members:
|
||||
|
||||
.. autoclass:: StateMachine
|
||||
:members:
|
||||
|
||||
.. autoclass:: ServiceCall
|
||||
:members:
|
||||
|
||||
.. autoclass:: ServiceRegistry
|
||||
:members:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: homeassistant.core
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -4,6 +4,14 @@ homeassistant.util package
|
||||
Submodules
|
||||
----------
|
||||
|
||||
homeassistant.util.async module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: homeassistant.util.async
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
homeassistant.util.color module
|
||||
-------------------------------
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ swagger: '2.0'
|
||||
info:
|
||||
title: Home Assistant
|
||||
description: Home Assistant REST API
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
# the domain of the service
|
||||
host: localhost:8123
|
||||
|
||||
@@ -12,17 +12,17 @@ schemes:
|
||||
- https
|
||||
|
||||
securityDefinitions:
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: api_password
|
||||
in: query
|
||||
|
||||
# api_key:
|
||||
#api_key:
|
||||
# type: apiKey
|
||||
# description: API password
|
||||
# name: x-ha-access
|
||||
# in: header
|
||||
# name: api_password
|
||||
# in: query
|
||||
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: x-ha-access
|
||||
in: header
|
||||
|
||||
# will be prefixed to all paths
|
||||
basePath: /api
|
||||
@@ -38,6 +38,8 @@ paths:
|
||||
description: Returns message if API is up and running.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: API is up and running
|
||||
@@ -53,6 +55,8 @@ paths:
|
||||
description: Returns the current configuration as JSON.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Current configuration
|
||||
@@ -81,6 +85,8 @@ paths:
|
||||
summary: Returns all data needed to bootstrap Home Assistant.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Bootstrap information
|
||||
@@ -96,6 +102,8 @@ paths:
|
||||
description: Returns an array of event objects. Each event object contain event name and listener count.
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Events
|
||||
@@ -113,6 +121,8 @@ paths:
|
||||
description: Returns an array of service objects. Each object contains the domain and which services it contains.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Services
|
||||
@@ -130,6 +140,8 @@ paths:
|
||||
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: State changes
|
||||
@@ -148,6 +160,8 @@ paths:
|
||||
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: States
|
||||
@@ -166,6 +180,8 @@ paths:
|
||||
Returns a state object for specified entity_id.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
@@ -223,6 +239,8 @@ paths:
|
||||
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
@@ -239,6 +257,8 @@ paths:
|
||||
Returns the data (image) from the specified camera entity_id.
|
||||
tags:
|
||||
- Camera
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- image/jpeg
|
||||
parameters:
|
||||
@@ -262,6 +282,8 @@ paths:
|
||||
Fires an event with event_type
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
@@ -286,6 +308,8 @@ paths:
|
||||
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
@@ -317,6 +341,8 @@ paths:
|
||||
Render a Home Assistant template.
|
||||
tags:
|
||||
- Template
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
@@ -338,6 +364,8 @@ paths:
|
||||
Setup event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
@@ -376,6 +404,8 @@ paths:
|
||||
tags:
|
||||
- Core
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/event-stream
|
||||
parameters:
|
||||
@@ -420,8 +450,16 @@ definitions:
|
||||
location_name:
|
||||
type: string
|
||||
unit_system:
|
||||
type: string
|
||||
description: The system for measurement units
|
||||
type: object
|
||||
properties:
|
||||
length:
|
||||
type: string
|
||||
mass:
|
||||
type: string
|
||||
temperature:
|
||||
type: string
|
||||
volume:
|
||||
type: string
|
||||
time_zone:
|
||||
type: string
|
||||
version:
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
__version__,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
REQUIRED_PYTHON_VER,
|
||||
REQUIRED_PYTHON_VER_WIN,
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
@@ -44,8 +45,7 @@ def monkey_patch_asyncio():
|
||||
See https://bugs.python.org/issue26617 for details of the Python
|
||||
bug.
|
||||
"""
|
||||
# pylint: disable=no-self-use, too-few-public-methods, protected-access
|
||||
# pylint: disable=bare-except
|
||||
# pylint: disable=no-self-use, protected-access, bare-except
|
||||
import asyncio.tasks
|
||||
|
||||
class IgnoreCalls:
|
||||
@@ -64,7 +64,12 @@ def monkey_patch_asyncio():
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate we're running the right Python version."""
|
||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
if sys.platform == "win32" and \
|
||||
sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER_WIN))
|
||||
sys.exit(1)
|
||||
elif sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER))
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Provides methods to bootstrap a home assistant instance."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from threading import RLock
|
||||
from collections import OrderedDict
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Any, Optional, Dict
|
||||
@@ -19,6 +18,9 @@ import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.util.async import (
|
||||
run_coroutine_threadsafe, run_callback_threadsafe)
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -26,33 +28,50 @@ from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SETUP_LOCK = RLock()
|
||||
_CURRENT_SETUP = []
|
||||
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
_PERSISTENT_ERRORS = {}
|
||||
HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)'
|
||||
|
||||
|
||||
def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_setup_component(hass, domain, config), loop=hass.loop).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if domain in hass.config.components:
|
||||
_LOGGER.debug('Component %s already set up.', domain)
|
||||
return True
|
||||
|
||||
_ensure_loader_prepared(hass)
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
if config is None:
|
||||
config = defaultdict(dict)
|
||||
config = {}
|
||||
|
||||
components = loader.load_order_component(domain)
|
||||
|
||||
# OrderedSet is empty if component or dependencies could not be resolved
|
||||
if not components:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
for component in components:
|
||||
if not _setup_component(hass, component, config):
|
||||
res = yield from _async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error('Component %s failed to setup', component)
|
||||
_async_persistent_notification(hass, component, True)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -60,7 +79,10 @@ def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
|
||||
def _handle_requirements(hass: core.HomeAssistant, component,
|
||||
name: str) -> bool:
|
||||
"""Install the requirements for a component."""
|
||||
"""Install the requirements for a component.
|
||||
|
||||
This method needs to run in an executor.
|
||||
"""
|
||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||
return True
|
||||
|
||||
@@ -68,70 +90,109 @@ def _handle_requirements(hass: core.HomeAssistant, component,
|
||||
if not pkg_util.install_package(req, target=hass.config.path('deps')):
|
||||
_LOGGER.error('Not initializing %s because could not install '
|
||||
'dependency %s', name, req)
|
||||
_async_persistent_notification(hass, name)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
||||
"""Setup a component for Home Assistant."""
|
||||
# pylint: disable=too-many-return-statements,too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
@asyncio.coroutine
|
||||
def _async_setup_component(hass: core.HomeAssistant,
|
||||
domain: str, config) -> bool:
|
||||
"""Setup a component for Home Assistant.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
|
||||
with _SETUP_LOCK:
|
||||
# It might have been loaded while waiting for lock
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
if domain in _CURRENT_SETUP:
|
||||
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
||||
domain, domain)
|
||||
return False
|
||||
setup_progress = hass.data.get('setup_progress')
|
||||
if setup_progress is None:
|
||||
setup_progress = hass.data['setup_progress'] = []
|
||||
|
||||
config = prepare_setup_component(hass, config, domain)
|
||||
if domain in setup_progress:
|
||||
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
||||
domain, domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Used to indicate to discovery that a setup is ongoing and allow it
|
||||
# to wait till it is done.
|
||||
did_lock = False
|
||||
if not setup_lock.locked():
|
||||
yield from setup_lock.acquire()
|
||||
did_lock = True
|
||||
|
||||
setup_progress.append(domain)
|
||||
config = yield from async_prepare_setup_component(hass, config, domain)
|
||||
|
||||
if config is None:
|
||||
return False
|
||||
|
||||
component = loader.get_component(domain)
|
||||
_CURRENT_SETUP.append(domain)
|
||||
if component is None:
|
||||
_async_persistent_notification(hass, domain)
|
||||
return False
|
||||
|
||||
async_comp = hasattr(component, 'async_setup')
|
||||
|
||||
try:
|
||||
result = component.setup(hass, config)
|
||||
if result is False:
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
return False
|
||||
elif result is not True:
|
||||
_LOGGER.error('component %s did not return boolean if setup '
|
||||
'was successful. Disabling component.', domain)
|
||||
loader.set_component(domain, None)
|
||||
return False
|
||||
_LOGGER.info("Setting up %s", domain)
|
||||
if async_comp:
|
||||
result = yield from component.async_setup(hass, config)
|
||||
else:
|
||||
result = yield from hass.loop.run_in_executor(
|
||||
None, component.setup, hass, config)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error during setup of component %s', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
if result is False:
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
elif result is not True:
|
||||
_LOGGER.error('component %s did not return boolean if setup '
|
||||
'was successful. Disabling component.', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
loader.set_component(domain, None)
|
||||
return False
|
||||
finally:
|
||||
_CURRENT_SETUP.remove(domain)
|
||||
|
||||
hass.config.components.append(component.DOMAIN)
|
||||
|
||||
# Assumption: if a component does not depend on groups
|
||||
# it communicates with devices
|
||||
if 'group' not in getattr(component, 'DEPENDENCIES', []) and \
|
||||
hass.pool.worker_count <= 10:
|
||||
hass.pool.add_worker()
|
||||
|
||||
hass.bus.fire(
|
||||
hass.bus.async_fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||
)
|
||||
|
||||
return True
|
||||
finally:
|
||||
setup_progress.remove(domain)
|
||||
if did_lock:
|
||||
setup_lock.release()
|
||||
|
||||
|
||||
def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_component(hass, config, domain), loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
component = loader.get_component(domain)
|
||||
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
||||
@@ -147,7 +208,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, domain, config)
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
return None
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
@@ -157,8 +218,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, domain, config)
|
||||
return None
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
continue
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
# So if p_name is None we are not going to validate platform
|
||||
@@ -167,20 +228,21 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
platforms.append(p_validated)
|
||||
continue
|
||||
|
||||
platform = prepare_setup_platform(hass, config, domain,
|
||||
p_name)
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, domain, p_name)
|
||||
|
||||
if platform is None:
|
||||
return None
|
||||
continue
|
||||
|
||||
# Validate platform specific schema
|
||||
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated)
|
||||
return None
|
||||
async_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated, hass)
|
||||
continue
|
||||
|
||||
platforms.append(p_validated)
|
||||
|
||||
@@ -191,7 +253,9 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
if key not in filter_keys}
|
||||
config[domain] = platforms
|
||||
|
||||
if not _handle_requirements(hass, component, domain):
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, component, domain)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return config
|
||||
@@ -200,7 +264,22 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) -> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup."""
|
||||
_ensure_loader_prepared(hass)
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_platform(hass, config, domain, platform_name),
|
||||
loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) \
|
||||
-> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
|
||||
|
||||
@@ -209,6 +288,7 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
# Not found
|
||||
if platform is None:
|
||||
_LOGGER.error('Unable to find platform %s', platform_path)
|
||||
_async_persistent_notification(hass, platform_path)
|
||||
return None
|
||||
|
||||
# Already loaded
|
||||
@@ -217,20 +297,23 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
|
||||
# Load dependencies
|
||||
for component in getattr(platform, 'DEPENDENCIES', []):
|
||||
if not setup_component(hass, component, config):
|
||||
res = yield from async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error(
|
||||
'Unable to prepare setup for platform %s because '
|
||||
'dependency %s could not be initialized', platform_path,
|
||||
component)
|
||||
_async_persistent_notification(hass, platform_path, True)
|
||||
return None
|
||||
|
||||
if not _handle_requirements(hass, platform, platform_path):
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, platform, platform_path)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return platform
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
hass: Optional[core.HomeAssistant]=None,
|
||||
config_dir: Optional[str]=None,
|
||||
@@ -250,15 +333,56 @@ def from_config_dict(config: Dict[str, Any],
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_dict(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.async_add_job(_async_init_from_config_dict(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
|
||||
return future.result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_from_config_dict(config: Dict[str, Any],
|
||||
hass: core.HomeAssistant,
|
||||
config_dir: Optional[str]=None,
|
||||
enable_log: bool=True,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=False,
|
||||
log_rotate_days: Any=None) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a config dict.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
hass.async_track_tasks()
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
yield from setup_lock.acquire()
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
conf_util.process_ha_core_config(hass, core_config)
|
||||
yield from conf_util.async_process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, 'homeassistant', core_config)
|
||||
async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
conf_util.process_ha_config_upgrade(hass)
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
if enable_log:
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
@@ -268,41 +392,45 @@ def from_config_dict(config: Dict[str, Any],
|
||||
_LOGGER.warning('Skipping pip installation of required modules. '
|
||||
'This may cause issues.')
|
||||
|
||||
_ensure_loader_prepared(hass)
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
# Convert it to defaultdict so components can always have config dict
|
||||
# Use OrderedDict in case original one was one.
|
||||
# Convert values to dictionaries if they are None
|
||||
config = defaultdict(
|
||||
dict, {key: value or {} for key, value in config.items()})
|
||||
new_config = OrderedDict()
|
||||
for key, value in config.items():
|
||||
new_config[key] = value or {}
|
||||
config = new_config
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
|
||||
# Setup in a thread to avoid blocking
|
||||
def component_setup():
|
||||
"""Set up a component."""
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
return hass
|
||||
# setup components
|
||||
# pylint: disable=not-an-iterable
|
||||
res = yield from core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
return hass
|
||||
|
||||
persistent_notification.setup(hass, config)
|
||||
yield from persistent_notification.async_setup(hass, config)
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
|
||||
# Setup the components
|
||||
for domain in loader.load_order_components(components):
|
||||
_setup_component(hass, domain, config)
|
||||
# Setup the components
|
||||
for domain in loader.load_order_components(components):
|
||||
yield from _async_setup_component(hass, domain, config)
|
||||
|
||||
setup_lock.release()
|
||||
|
||||
yield from hass.async_stop_track_tasks()
|
||||
|
||||
hass.loop.run_until_complete(
|
||||
hass.loop.run_in_executor(None, component_setup)
|
||||
)
|
||||
return hass
|
||||
|
||||
|
||||
@@ -319,30 +447,71 @@ def from_config_file(config_path: str,
|
||||
if hass is None:
|
||||
hass = core.HomeAssistant()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_file(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.loop.create_task(_async_init_from_config_file(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
|
||||
return future.result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_from_config_file(config_path: str,
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=True,
|
||||
log_rotate_days: Any=None):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, mount_local_lib_path, config_dir)
|
||||
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
try:
|
||||
config_dict = conf_util.load_yaml_config_file(config_path)
|
||||
config_dict = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError:
|
||||
return None
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
|
||||
return from_config_dict(config_dict, hass, enable_log=False,
|
||||
skip_pip=skip_pip)
|
||||
hass = yield from async_from_config_dict(
|
||||
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
||||
return hass
|
||||
|
||||
|
||||
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
log_rotate_days=None) -> None:
|
||||
"""Setup the logging."""
|
||||
"""Setup the logging.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
"[%(name)s] %(message)s%(reset)s")
|
||||
|
||||
# suppress overly verbose logs from libraries that aren't helpful
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
@@ -360,6 +529,10 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# AsyncHandler allready exists?
|
||||
if hass.data.get(core.DATA_ASYNCHANDLER):
|
||||
return
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
err_path_exists = os.path.isfile(err_log_path)
|
||||
@@ -380,8 +553,12 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
err_handler.setFormatter(
|
||||
logging.Formatter('%(asctime)s %(name)s: %(message)s',
|
||||
datefmt='%y-%m-%d %H:%M:%S'))
|
||||
|
||||
async_handler = AsyncHandler(hass.loop, err_handler)
|
||||
hass.data[core.DATA_ASYNCHANDLER] = async_handler
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(err_handler)
|
||||
logger.addHandler(async_handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
else:
|
||||
@@ -389,36 +566,62 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
'Unable to setup error log %s (access denied)', err_log_path)
|
||||
|
||||
|
||||
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
||||
"""Ensure Home Assistant loader is prepared."""
|
||||
if not loader.PREPARED:
|
||||
loader.prepare(hass)
|
||||
|
||||
|
||||
def log_exception(ex, domain, config):
|
||||
def log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_log_exception, ex, domain, config, hass).result()
|
||||
|
||||
|
||||
@core.callback
|
||||
def _async_persistent_notification(hass: core.HomeAssistant, component: str,
|
||||
link: Optional[bool]=False):
|
||||
"""Print a persistent notification.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
_PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link
|
||||
_lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name)
|
||||
if link else name for name, link in _PERSISTENT_ERRORS.items()]
|
||||
message = ('The following components and platforms could not be set up:\n'
|
||||
'* ' + '\n* '.join(list(_lst)) + '\nPlease check your config')
|
||||
persistent_notification.async_create(
|
||||
hass, message, 'Invalid config', 'invalid_config')
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
if hass is not None:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||
.format(ex.path[-1], domain, domain,
|
||||
'->'.join('%s' % m for m in ex.path))
|
||||
'->'.join(str(m) for m in ex.path))
|
||||
else:
|
||||
message += '{}.'.format(humanize_error(config, ex))
|
||||
|
||||
if hasattr(config, '__line__'):
|
||||
message += " (See {}:{})".format(
|
||||
config.__config_file__, config.__line__ or '?')
|
||||
domain_config = config.get(domain, config)
|
||||
message += " (See {}:{}). ".format(
|
||||
getattr(domain_config, '__config_file__', '?'),
|
||||
getattr(domain_config, '__line__', '?'))
|
||||
|
||||
if domain != 'homeassistant':
|
||||
message += (' Please check the docs at '
|
||||
message += ('Please check the docs at '
|
||||
'https://home-assistant.io/components/{}/'.format(domain))
|
||||
|
||||
_LOGGER.error(message)
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path."""
|
||||
"""Add local library to Python Path.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
if deps_dir not in sys.path:
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
|
||||
@@ -7,6 +7,7 @@ Component design guidelines:
|
||||
format "<DOMAIN>.<OBJECT_ID>".
|
||||
- Each component should publish services only under its own domain.
|
||||
"""
|
||||
import asyncio
|
||||
import itertools as it
|
||||
import logging
|
||||
|
||||
@@ -79,8 +80,10 @@ def reload_core_config(hass):
|
||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup general services related to Home Assistant."""
|
||||
@asyncio.coroutine
|
||||
def handle_turn_service(service):
|
||||
"""Method to handle calls to homeassistant.turn_on/off."""
|
||||
entity_ids = extract_entity_ids(hass, service)
|
||||
@@ -96,6 +99,8 @@ def setup(hass, config):
|
||||
by_domain = it.groupby(sorted(entity_ids),
|
||||
lambda item: ha.split_entity_id(item)[0])
|
||||
|
||||
tasks = []
|
||||
|
||||
for domain, ent_ids in by_domain:
|
||||
# We want to block for all calls and only return when all calls
|
||||
# have been processed. If a service does not exist it causes a 10
|
||||
@@ -111,27 +116,34 @@ def setup(hass, config):
|
||||
# ent_ids is a generator, convert it to a list.
|
||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||
|
||||
hass.services.call(domain, service.service, data, blocking)
|
||||
tasks.append(hass.services.async_call(
|
||||
domain, service.service, data, blocking))
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import config as conf_util
|
||||
|
||||
try:
|
||||
path = conf_util.find_config_file(hass.config.config_dir)
|
||||
conf = conf_util.load_yaml_config_file(path)
|
||||
conf = yield from conf_util.async_hass_config_yaml(hass)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
|
||||
yield from conf_util.async_process_ha_core_config(
|
||||
hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
|
||||
handle_reload_config)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -4,6 +4,7 @@ Component to interface with an alarm control panel.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -42,36 +43,6 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
def alarm_service_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
target_alarms = component.extract_from_service(service)
|
||||
|
||||
code = service.data.get(ATTR_CODE)
|
||||
|
||||
method = SERVICE_TO_METHOD[service.service]
|
||||
|
||||
for alarm in target_alarms:
|
||||
getattr(alarm, method)(code)
|
||||
if alarm.should_poll:
|
||||
alarm.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.register(DOMAIN, service, alarm_service_handler,
|
||||
descriptions.get(service),
|
||||
schema=ALARM_SERVICE_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
def alarm_disarm(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for disarm."""
|
||||
data = {}
|
||||
@@ -116,6 +87,53 @@ def alarm_trigger(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_service_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
target_alarms = component.async_extract_from_service(service)
|
||||
|
||||
code = service.data.get(ATTR_CODE)
|
||||
|
||||
method = "async_{}".format(SERVICE_TO_METHOD[service.service])
|
||||
|
||||
for alarm in target_alarms:
|
||||
yield from getattr(alarm, method)(code)
|
||||
|
||||
update_tasks = []
|
||||
for alarm in target_alarms:
|
||||
if not alarm.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
alarm.async_update_ha_state(True))
|
||||
if hasattr(alarm, 'async_update'):
|
||||
update_tasks.append(hass.loop.create_task(update_coro))
|
||||
else:
|
||||
yield from update_coro
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, 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)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
"""An abstract class for alarm control devices."""
|
||||
@@ -134,18 +152,42 @@ class AlarmControlPanel(Entity):
|
||||
"""Send disarm command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, self.alarm_disarm, code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, self.alarm_arm_home, code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, self.alarm_arm_away, code)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command."""
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, self.alarm_trigger, code)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
||||
@@ -39,11 +39,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
add_devices([AlarmDotCom(hass, name, code, username, password)])
|
||||
add_devices([AlarmDotCom(hass, name, code, username, password)], True)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
"""Represent an Alarm.com status."""
|
||||
|
||||
@@ -56,11 +54,11 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return True
|
||||
def update(self):
|
||||
"""Fetch the latest state."""
|
||||
self._state = self._alarm.state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -75,11 +73,11 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._alarm.state == 'Disarmed':
|
||||
if self._state == 'Disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
elif self._alarm.state == 'Armed Stay':
|
||||
elif self._state == 'Armed Stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state == 'Armed Away':
|
||||
elif self._state == 'Armed Away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
123
homeassistant/components/alarm_control_panel/concord232.py
Executable file
123
homeassistant/components/alarm_control_panel/concord232.py
Executable file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Support for Concord232 alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.concord232/
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import requests
|
||||
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_PORT, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = 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,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Concord232 alarm control panel platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
|
||||
try:
|
||||
add_devices([Concord232Alarm(hass, url, name)])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return False
|
||||
|
||||
|
||||
class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
"""Represents the Concord232-based alarm panel."""
|
||||
|
||||
def __init__(self, hass, url, name):
|
||||
"""Initialize the Concord232 alarm panel."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._url = url
|
||||
|
||||
try:
|
||||
client = concord232_client.Client(self._url)
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
|
||||
self._alarm = client
|
||||
self._alarm.partitions = self._alarm.list_partitions()
|
||||
self._alarm.last_partition_update = datetime.datetime.now()
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update values from API."""
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex))
|
||||
newstate = STATE_UNKNOWN
|
||||
except IndexError:
|
||||
_LOGGER.error("Concord232 reports no partitions")
|
||||
newstate = STATE_UNKNOWN
|
||||
|
||||
if part['arming_level'] == 'Off':
|
||||
newstate = STATE_ALARM_DISARMED
|
||||
elif 'Home' in part['arming_level']:
|
||||
newstate = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
newstate = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
if not newstate == self._state:
|
||||
_LOGGER.info("State Chnage from %s to %s", self._state, newstate)
|
||||
self._state = newstate
|
||||
return self._state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._alarm.disarm(code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('home')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('auto')
|
||||
@@ -4,26 +4,48 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
from os import path
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
EnvisalinkDevice,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_CODE,
|
||||
CONF_PANIC,
|
||||
CONF_PARTITIONNAME,
|
||||
SIGNAL_PARTITION_UPDATE,
|
||||
SIGNAL_KEYPAD_UPDATE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.components.envisalink import (
|
||||
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEVICES = []
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
|
||||
ATTR_KEYPRESS = 'keypress'
|
||||
ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_KEYPRESS): cv.string
|
||||
})
|
||||
|
||||
|
||||
def alarm_keypress_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
keypress = service.data.get(ATTR_KEYPRESS)
|
||||
|
||||
_target_devices = [device for device in DEVICES
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
for device in _target_devices:
|
||||
EnvisalinkAlarm.alarm_keypress(device, keypress)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
_code = discovery_info[CONF_CODE]
|
||||
@@ -38,74 +60,105 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
_panic_type,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
DEVICES.append(_device)
|
||||
|
||||
add_devices(DEVICES)
|
||||
|
||||
# Register Envisalink specific services
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(alarm.DOMAIN, SERVICE_ALARM_KEYPRESS,
|
||||
alarm_keypress_handler,
|
||||
descriptions.get(SERVICE_ALARM_KEYPRESS),
|
||||
schema=ALARM_KEYPRESS_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Represents the Envisalink-based alarm panel."""
|
||||
"""Representation of an Envisalink-based alarm panel."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, partition_number, alarm_name,
|
||||
code, panic_type, info, controller):
|
||||
def __init__(self, partition_number, alarm_name, code, panic_type, info,
|
||||
controller):
|
||||
"""Initialize the alarm panel."""
|
||||
from pydispatch import dispatcher
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
self._panic_type = panic_type
|
||||
_LOGGER.debug('Setting up alarm: ' + alarm_name)
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(
|
||||
self._update_callback, signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(
|
||||
self._update_callback, signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
def _update_callback(self, partition):
|
||||
"""Update HA state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.hass.async_add_job(self.update_ha_state)
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return self._code
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
else:
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
state = STATE_UNKNOWN
|
||||
|
||||
if self._info['status']['alarm']:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
elif self._info['status']['armed_away']:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif self._info['status']['armed_stay']:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif self._info['status']['exit_delay']:
|
||||
state = STATE_ALARM_PENDING
|
||||
elif self._info['status']['entry_delay']:
|
||||
state = STATE_ALARM_PENDING
|
||||
elif self._info['status']['alpha']:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
state = STATE_ALARM_DISARMED
|
||||
return state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._code:
|
||||
if code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.disarm_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if self._code:
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._code:
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_away_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
||||
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
||||
|
||||
def alarm_keypress(self, keypress=None):
|
||||
"""Send custom keypress."""
|
||||
if keypress:
|
||||
EVL_CONTROLLER.keypresses_to_partition(self._partition_number,
|
||||
keypress)
|
||||
|
||||
@@ -50,8 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class ManualAlarm(alarm.AlarmControlPanel):
|
||||
"""
|
||||
Represents an alarm status.
|
||||
@@ -118,7 +116,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
@@ -127,11 +125,11 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
@@ -141,11 +139,11 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
@@ -153,15 +151,15 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._pre_trigger_state = self._state
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._trigger_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time + self._trigger_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
|
||||
@@ -55,8 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
config.get(CONF_CODE))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class MqttAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
|
||||
@@ -60,11 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
# talk to the API and trigger a requests exception for setup_platform()
|
||||
# to catch
|
||||
self._alarm.list_zones()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling needed."""
|
||||
return True
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -79,16 +75,20 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Process new events from panel."""
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
zones = self._alarm.list_zones()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||
dict(host=self._url, reason=ex))
|
||||
return STATE_UNKNOWN
|
||||
self._state = STATE_UNKNOWN
|
||||
except IndexError:
|
||||
_LOGGER.error('nx584 reports no partitions')
|
||||
return STATE_UNKNOWN
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
bypassed = False
|
||||
for zone in zones:
|
||||
@@ -100,11 +100,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
break
|
||||
|
||||
if not part['armed']:
|
||||
return STATE_ALARM_DISARMED
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif bypassed:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
@@ -112,12 +112,8 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('home')
|
||||
self._alarm.arm('stay')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('auto')
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command."""
|
||||
raise NotImplementedError()
|
||||
self._alarm.arm('exit')
|
||||
|
||||
@@ -41,3 +41,14 @@ alarm_trigger:
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
envisalink_alarm_keypress:
|
||||
description: Send custom keypresses to the alarm
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel (1-6 characters)'
|
||||
example: '*71'
|
||||
|
||||
@@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([SimpliSafeAlarm(name, username, password, code)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation a SimpliSafe alarm."""
|
||||
|
||||
@@ -62,11 +61,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll the SimpliSafe API."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
@@ -105,7 +99,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
return
|
||||
self.simplisafe.set_state('off')
|
||||
_LOGGER.info('SimpliSafe alarm disarming')
|
||||
self.update()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
@@ -113,7 +106,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
return
|
||||
self.simplisafe.set_state('home')
|
||||
_LOGGER.info('SimpliSafe alarm arming home')
|
||||
self.update()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
@@ -121,7 +113,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
return
|
||||
self.simplisafe.set_state('away')
|
||||
_LOGGER.info('SimpliSafe alarm arming away')
|
||||
self.update()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
|
||||
@@ -28,7 +28,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(alarms)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
"""Represent a Verisure alarm status."""
|
||||
|
||||
@@ -85,18 +84,15 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
hub.my_pages.alarm.set(code, 'DISARMED')
|
||||
_LOGGER.info('verisure alarm disarming')
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_HOME')
|
||||
_LOGGER.info('verisure alarm arming home')
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
|
||||
_LOGGER.info('verisure alarm arming away')
|
||||
hub.my_pages.alarm.wait_while_pending()
|
||||
self.update()
|
||||
|
||||
@@ -4,19 +4,25 @@ 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 copy
|
||||
import enum
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_ENDPOINT = '/api/alexa'
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
|
||||
|
||||
CONF_ACTION = 'action'
|
||||
CONF_CARD = 'card'
|
||||
@@ -28,6 +34,23 @@ CONF_TITLE = 'title'
|
||||
CONF_CONTENT = 'content'
|
||||
CONF_TEXT = 'text'
|
||||
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
CONF_UID = 'uid'
|
||||
CONF_DATE = 'date'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_AUDIO = 'audio'
|
||||
CONF_TEXT = 'text'
|
||||
CONF_DISPLAY_URL = 'display_url'
|
||||
|
||||
ATTR_UID = 'uid'
|
||||
ATTR_UPDATE_DATE = 'updateDate'
|
||||
ATTR_TITLE_TEXT = 'titleText'
|
||||
ATTR_STREAM_URL = 'streamUrl'
|
||||
ATTR_MAIN_TEXT = 'mainText'
|
||||
ATTR_REDIRECTION_URL = 'redirectionURL'
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
@@ -61,6 +84,16 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TEXT): cv.template,
|
||||
}
|
||||
}
|
||||
},
|
||||
CONF_FLASH_BRIEFINGS: {
|
||||
cv.string: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
|
||||
vol.Optional(CONF_DATE, default=datetime.utcnow()): cv.string,
|
||||
vol.Required(CONF_TITLE): cv.template,
|
||||
vol.Optional(CONF_AUDIO): cv.template,
|
||||
vol.Required(CONF_TEXT, default=""): cv.template,
|
||||
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
||||
}]),
|
||||
}
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@@ -68,21 +101,24 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
def setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
hass.wsgi.register_view(AlexaView(hass,
|
||||
config[DOMAIN].get(CONF_INTENTS, {})))
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
|
||||
|
||||
hass.http.register_view(AlexaIntentsView(hass, intents))
|
||||
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlexaView(HomeAssistantView):
|
||||
class AlexaIntentsView(HomeAssistantView):
|
||||
"""Handle Alexa requests."""
|
||||
|
||||
url = API_ENDPOINT
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:alexa'
|
||||
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
super().__init__()
|
||||
|
||||
intents = copy.deepcopy(intents)
|
||||
template.attach(hass, intents)
|
||||
@@ -94,9 +130,10 @@ class AlexaView(HomeAssistantView):
|
||||
|
||||
self.intents = intents
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
data = request.json
|
||||
data = yield from request.json()
|
||||
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
|
||||
@@ -113,7 +150,7 @@ class AlexaView(HomeAssistantView):
|
||||
return None
|
||||
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(self.hass, intent)
|
||||
response = AlexaResponse(request.app['hass'], intent)
|
||||
|
||||
if req_type == 'LaunchRequest':
|
||||
response.add_speech(
|
||||
@@ -142,7 +179,7 @@ class AlexaView(HomeAssistantView):
|
||||
action = config.get(CONF_ACTION)
|
||||
|
||||
if action is not None:
|
||||
action.run(response.variables)
|
||||
yield from action.async_run(response.variables)
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
@@ -184,8 +221,8 @@ class AlexaResponse(object):
|
||||
self.card = card
|
||||
return
|
||||
|
||||
card["title"] = title.render(self.variables)
|
||||
card["content"] = content.render(self.variables)
|
||||
card["title"] = title.async_render(self.variables)
|
||||
card["content"] = content.async_render(self.variables)
|
||||
self.card = card
|
||||
|
||||
def add_speech(self, speech_type, text):
|
||||
@@ -195,7 +232,7 @@ class AlexaResponse(object):
|
||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||
|
||||
if isinstance(text, template.Template):
|
||||
text = text.render(self.variables)
|
||||
text = text.async_render(self.variables)
|
||||
|
||||
self.speech = {
|
||||
'type': speech_type.value,
|
||||
@@ -210,7 +247,7 @@ class AlexaResponse(object):
|
||||
|
||||
self.reprompt = {
|
||||
'type': speech_type.value,
|
||||
key: text.render(self.variables)
|
||||
key: text.async_render(self.variables)
|
||||
}
|
||||
|
||||
def as_dict(self):
|
||||
@@ -235,3 +272,69 @@ class AlexaResponse(object):
|
||||
'sessionAttributes': self.session_attributes,
|
||||
'response': response,
|
||||
}
|
||||
|
||||
|
||||
class AlexaFlashBriefingView(HomeAssistantView):
|
||||
"""Handle Alexa Flash Briefing skill requests."""
|
||||
|
||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||
name = 'api:alexa:flash_briefings'
|
||||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__()
|
||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
@callback
|
||||
def get(self, request, briefing_id):
|
||||
"""Handle Alexa Flash Briefing request."""
|
||||
_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'
|
||||
_LOGGER.error(err, briefing_id)
|
||||
return b'', 404
|
||||
|
||||
briefing = []
|
||||
|
||||
for item in self.flash_briefings.get(briefing_id, []):
|
||||
output = {}
|
||||
if item.get(CONF_TITLE) is not None:
|
||||
if isinstance(item.get(CONF_TITLE), template.Template):
|
||||
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render()
|
||||
else:
|
||||
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
|
||||
|
||||
if item.get(CONF_TEXT) is not None:
|
||||
if isinstance(item.get(CONF_TEXT), template.Template):
|
||||
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render()
|
||||
else:
|
||||
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
|
||||
|
||||
if item.get(CONF_UID) is not None:
|
||||
output[ATTR_UID] = item.get(CONF_UID)
|
||||
|
||||
if item.get(CONF_AUDIO) is not None:
|
||||
if isinstance(item.get(CONF_AUDIO), template.Template):
|
||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render()
|
||||
else:
|
||||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||
|
||||
if item.get(CONF_DISPLAY_URL) is not None:
|
||||
if isinstance(item.get(CONF_DISPLAY_URL),
|
||||
template.Template):
|
||||
output[ATTR_REDIRECTION_URL] = \
|
||||
item[CONF_DISPLAY_URL].async_render()
|
||||
else:
|
||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||
|
||||
if isinstance(item[CONF_DATE], str):
|
||||
item[CONF_DATE] = dt_util.parse_datetime(item[CONF_DATE])
|
||||
|
||||
output[ATTR_UPDATE_DATE] = item[CONF_DATE].strftime(DATE_FORMAT)
|
||||
|
||||
briefing.append(output)
|
||||
|
||||
return self.json(briefing)
|
||||
|
||||
@@ -7,7 +7,9 @@ https://home-assistant.io/developers/api/
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
@@ -21,7 +23,7 @@ from homeassistant.const import (
|
||||
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import TrackStates
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
@@ -36,20 +38,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup(hass, config):
|
||||
"""Register the API with the HTTP interface."""
|
||||
hass.wsgi.register_view(APIStatusView)
|
||||
hass.wsgi.register_view(APIEventStream)
|
||||
hass.wsgi.register_view(APIConfigView)
|
||||
hass.wsgi.register_view(APIDiscoveryView)
|
||||
hass.wsgi.register_view(APIStatesView)
|
||||
hass.wsgi.register_view(APIEntityStateView)
|
||||
hass.wsgi.register_view(APIEventListenersView)
|
||||
hass.wsgi.register_view(APIEventView)
|
||||
hass.wsgi.register_view(APIServicesView)
|
||||
hass.wsgi.register_view(APIDomainServicesView)
|
||||
hass.wsgi.register_view(APIEventForwardingView)
|
||||
hass.wsgi.register_view(APIComponentsView)
|
||||
hass.wsgi.register_view(APIErrorLogView)
|
||||
hass.wsgi.register_view(APITemplateView)
|
||||
hass.http.register_view(APIStatusView)
|
||||
hass.http.register_view(APIEventStream)
|
||||
hass.http.register_view(APIConfigView)
|
||||
hass.http.register_view(APIDiscoveryView)
|
||||
hass.http.register_view(APIStatesView)
|
||||
hass.http.register_view(APIEntityStateView)
|
||||
hass.http.register_view(APIEventListenersView)
|
||||
hass.http.register_view(APIEventView)
|
||||
hass.http.register_view(APIServicesView)
|
||||
hass.http.register_view(APIDomainServicesView)
|
||||
hass.http.register_view(APIEventForwardingView)
|
||||
hass.http.register_view(APIComponentsView)
|
||||
hass.http.register_view(APIErrorLogView)
|
||||
hass.http.register_view(APITemplateView)
|
||||
|
||||
return True
|
||||
|
||||
@@ -60,6 +62,7 @@ class APIStatusView(HomeAssistantView):
|
||||
url = URL_API
|
||||
name = "api:status"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Retrieve if API is running."""
|
||||
return self.json_message('API running.')
|
||||
@@ -71,12 +74,15 @@ class APIEventStream(HomeAssistantView):
|
||||
url = URL_API_STREAM
|
||||
name = "api:stream"
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
# pylint: disable=no-self-use
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = queue.Queue()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
|
||||
restrict = request.args.get('restrict')
|
||||
restrict = request.GET.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||
|
||||
@@ -96,38 +102,40 @@ class APIEventStream(HomeAssistantView):
|
||||
else:
|
||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||
|
||||
to_write.put(data)
|
||||
yield from to_write.put(data)
|
||||
|
||||
def stream():
|
||||
"""Stream events to response."""
|
||||
unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events)
|
||||
response = web.StreamResponse()
|
||||
response.content_type = 'text/event-stream'
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||
|
||||
# Fire off one message so browsers fire open event right away
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
try:
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
|
||||
while True:
|
||||
try:
|
||||
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
|
||||
# Fire off one message so browsers fire open event right away
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
if payload is stop_obj:
|
||||
break
|
||||
while True:
|
||||
try:
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||
loop=hass.loop):
|
||||
payload = yield from to_write.get()
|
||||
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
yield msg.encode("UTF-8")
|
||||
except queue.Empty:
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
except GeneratorExit:
|
||||
if payload is stop_obj:
|
||||
break
|
||||
finally:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
unsub_stream()
|
||||
|
||||
return self.Response(stream(), mimetype='text/event-stream')
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
response.write(msg.encode("UTF-8"))
|
||||
yield from response.drain()
|
||||
except asyncio.TimeoutError:
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
finally:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
unsub_stream()
|
||||
|
||||
|
||||
class APIConfigView(HomeAssistantView):
|
||||
@@ -136,9 +144,10 @@ class APIConfigView(HomeAssistantView):
|
||||
url = URL_API_CONFIG
|
||||
name = "api:config"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current configuration."""
|
||||
return self.json(self.hass.config.as_dict())
|
||||
return self.json(request.app['hass'].config.as_dict())
|
||||
|
||||
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
@@ -148,12 +157,14 @@ class APIDiscoveryView(HomeAssistantView):
|
||||
url = URL_API_DISCOVERY_INFO
|
||||
name = "api:discovery"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get discovery info."""
|
||||
needs_auth = self.hass.config.api.api_password is not None
|
||||
hass = request.app['hass']
|
||||
needs_auth = hass.config.api.api_password is not None
|
||||
return self.json({
|
||||
'base_url': self.hass.config.api.base_url,
|
||||
'location_name': self.hass.config.location_name,
|
||||
'base_url': hass.config.api.base_url,
|
||||
'location_name': hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
})
|
||||
@@ -165,53 +176,62 @@ class APIStatesView(HomeAssistantView):
|
||||
url = URL_API_STATES
|
||||
name = "api:states"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current states."""
|
||||
return self.json(self.hass.states.all())
|
||||
return self.json(request.app['hass'].states.async_all())
|
||||
|
||||
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
"""View to handle EntityState requests."""
|
||||
|
||||
url = "/api/states/<entity(exist=False):entity_id>"
|
||||
url = "/api/states/{entity_id}"
|
||||
name = "api:entity-state"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve state of entity."""
|
||||
state = self.hass.states.get(entity_id)
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
new_state = request.json['state']
|
||||
except KeyError:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
new_state = data.get('state')
|
||||
|
||||
if not new_state:
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = request.json.get('attributes')
|
||||
force_update = request.json.get('force_update', False)
|
||||
attributes = data.get('attributes')
|
||||
force_update = data.get('force_update', False)
|
||||
|
||||
is_new_state = self.hass.states.get(entity_id) is None
|
||||
is_new_state = hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
self.hass.states.set(entity_id, new_state, attributes, force_update)
|
||||
hass.states.async_set(entity_id, new_state, attributes, force_update)
|
||||
|
||||
# Read the state back for our response
|
||||
resp = self.json(self.hass.states.get(entity_id))
|
||||
|
||||
if is_new_state:
|
||||
resp.status_code = HTTP_CREATED
|
||||
status_code = HTTP_CREATED if is_new_state else 200
|
||||
resp = self.json(hass.states.get(entity_id), status_code)
|
||||
|
||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
return resp
|
||||
|
||||
@ha.callback
|
||||
def delete(self, request, entity_id):
|
||||
"""Remove entity."""
|
||||
if self.hass.states.remove(entity_id):
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
@@ -223,20 +243,23 @@ class APIEventListenersView(HomeAssistantView):
|
||||
url = URL_API_EVENTS
|
||||
name = "api:event-listeners"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get event listeners."""
|
||||
return self.json(events_json(self.hass))
|
||||
return self.json(async_events_json(request.app['hass']))
|
||||
|
||||
|
||||
class APIEventView(HomeAssistantView):
|
||||
"""View to handle Event requests."""
|
||||
|
||||
url = '/api/events/<event_type>'
|
||||
url = '/api/events/{event_type}'
|
||||
name = "api:event"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
event_data = request.json
|
||||
body = yield from request.text()
|
||||
event_data = json.loads(body) if body else None
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
return self.json_message('Event data should be a JSON object',
|
||||
@@ -251,7 +274,8 @@ class APIEventView(HomeAssistantView):
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
|
||||
request.app['hass'].bus.async_fire(event_type, event_data,
|
||||
ha.EventOrigin.remote)
|
||||
|
||||
return self.json_message("Event {} fired.".format(event_type))
|
||||
|
||||
@@ -262,24 +286,30 @@ class APIServicesView(HomeAssistantView):
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get registered services."""
|
||||
return self.json(services_json(self.hass))
|
||||
return self.json(async_services_json(request.app['hass']))
|
||||
|
||||
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
"""View to handle DomainServices requests."""
|
||||
|
||||
url = "/api/services/<domain>/<service>"
|
||||
url = "/api/services/{domain}/{service}"
|
||||
name = "api:domain-services"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, domain, service):
|
||||
"""Call a service.
|
||||
|
||||
Returns a list of changed states.
|
||||
"""
|
||||
with TrackStates(self.hass) as changed_states:
|
||||
self.hass.services.call(domain, service, request.json, True)
|
||||
hass = request.app['hass']
|
||||
body = yield from request.text()
|
||||
data = json.loads(body) if body else None
|
||||
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
yield from hass.services.async_call(domain, service, data, True)
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
@@ -291,11 +321,15 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
name = "api:event-forward"
|
||||
event_forwarder = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
data = request.json
|
||||
if data is None:
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
@@ -311,21 +345,25 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
if not api.validate_api():
|
||||
valid = yield from hass.loop.run_in_executor(
|
||||
None, api.validate_api)
|
||||
if not valid:
|
||||
return self.json_message("Unable to validate API.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self.event_forwarder is None:
|
||||
self.event_forwarder = rem.EventForwarder(self.hass)
|
||||
self.event_forwarder = rem.EventForwarder(hass)
|
||||
|
||||
self.event_forwarder.connect(api)
|
||||
self.event_forwarder.async_connect(api)
|
||||
|
||||
return self.json_message("Event forwarding setup.")
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete(self, request):
|
||||
"""Remove event forwarer."""
|
||||
data = request.json
|
||||
if data is None:
|
||||
"""Remove event forwarder."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
@@ -342,7 +380,7 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
if self.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
self.event_forwarder.disconnect(api)
|
||||
self.event_forwarder.async_disconnect(api)
|
||||
|
||||
return self.json_message("Event forwarding cancelled.")
|
||||
|
||||
@@ -353,9 +391,10 @@ class APIComponentsView(HomeAssistantView):
|
||||
url = URL_API_COMPONENTS
|
||||
name = "api:components"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current loaded components."""
|
||||
return self.json(self.hass.config.components)
|
||||
return self.json(request.app['hass'].config.components)
|
||||
|
||||
|
||||
class APIErrorLogView(HomeAssistantView):
|
||||
@@ -364,9 +403,12 @@ class APIErrorLogView(HomeAssistantView):
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error-log"
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Serve error log."""
|
||||
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
|
||||
resp = yield from self.file(
|
||||
request, request.app['hass'].config.path(ERROR_LOG_FILENAME))
|
||||
return resp
|
||||
|
||||
|
||||
class APITemplateView(HomeAssistantView):
|
||||
@@ -375,23 +417,25 @@ class APITemplateView(HomeAssistantView):
|
||||
url = URL_API_TEMPLATE
|
||||
name = "api:template"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Render a template."""
|
||||
try:
|
||||
tpl = template.Template(request.json['template'], self.hass)
|
||||
return tpl.render(request.json.get('variables'))
|
||||
except TemplateError as ex:
|
||||
data = yield from request.json()
|
||||
tpl = template.Template(data['template'], request.app['hass'])
|
||||
return tpl.async_render(data.get('variables'))
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message('Error rendering template: {}'.format(ex),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
def services_json(hass):
|
||||
def async_services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
return [{"domain": key, "services": value}
|
||||
for key, value in hass.services.services.items()]
|
||||
for key, value in hass.services.async_services().items()]
|
||||
|
||||
|
||||
def events_json(hass):
|
||||
def async_events_json(hass):
|
||||
"""Generate event data to JSONify."""
|
||||
return [{"event": key, "listener_count": value}
|
||||
for key, value in hass.bus.listeners.items()]
|
||||
for key, value in hass.bus.async_listeners().items()]
|
||||
|
||||
@@ -11,7 +11,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.bootstrap import async_prepare_setup_platform
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
@@ -24,7 +24,6 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.loader import get_platform
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@@ -67,6 +66,7 @@ def _platform_validator(config):
|
||||
|
||||
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
||||
|
||||
|
||||
_TRIGGER_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
@@ -143,68 +143,81 @@ def reload(hass):
|
||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the automation."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
||||
|
||||
success = run_coroutine_threadsafe(
|
||||
_async_process_config(hass, config, component), hass.loop).result()
|
||||
success = yield from _async_process_config(hass, config, component)
|
||||
|
||||
if not success:
|
||||
return False
|
||||
|
||||
descriptions = conf_util.load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, 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."""
|
||||
for entity in component.extract_from_service(service_call):
|
||||
hass.loop.create_task(entity.async_trigger(
|
||||
tasks = []
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES), True))
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def turn_onoff_service_handler(service_call):
|
||||
"""Handle automation turn on/off service calls."""
|
||||
tasks = []
|
||||
method = 'async_{}'.format(service_call.service)
|
||||
for entity in component.extract_from_service(service_call):
|
||||
hass.loop.create_task(getattr(entity, method)())
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(getattr(entity, method)())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def toggle_service_handler(service_call):
|
||||
"""Handle automation toggle service calls."""
|
||||
for entity in component.extract_from_service(service_call):
|
||||
tasks = []
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
if entity.is_on:
|
||||
hass.loop.create_task(entity.async_turn_off())
|
||||
tasks.append(entity.async_turn_off())
|
||||
else:
|
||||
hass.loop.create_task(entity.async_turn_on())
|
||||
tasks.append(entity.async_turn_on())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
"""Remove all automations and load new ones from config."""
|
||||
conf = yield from hass.loop.run_in_executor(
|
||||
None, component.prepare_reload)
|
||||
conf = yield from component.async_prepare_reload()
|
||||
if conf is None:
|
||||
return
|
||||
hass.loop.create_task(_async_process_config(hass, conf, component))
|
||||
yield from _async_process_config(hass, conf, component)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
descriptions.get(SERVICE_TRIGGER),
|
||||
schema=TRIGGER_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions.get(SERVICE_RELOAD),
|
||||
schema=RELOAD_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||
descriptions.get(SERVICE_TOGGLE),
|
||||
schema=SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||
descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA)
|
||||
|
||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
||||
hass.services.register(DOMAIN, service, turn_onoff_service_handler,
|
||||
descriptions.get(service),
|
||||
schema=SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, turn_onoff_service_handler,
|
||||
descriptions.get(service), schema=SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -212,8 +225,6 @@ def setup(hass, config):
|
||||
class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||
hidden):
|
||||
"""Initialize an automation entity."""
|
||||
@@ -260,7 +271,7 @@ class AutomationEntity(ToggleEntity):
|
||||
return
|
||||
|
||||
yield from self.async_enable()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs) -> None:
|
||||
@@ -271,7 +282,7 @@ class AutomationEntity(ToggleEntity):
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
self._enabled = False
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(self, variables, skip_condition=False):
|
||||
@@ -280,15 +291,15 @@ class AutomationEntity(ToggleEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if skip_condition or self._cond_func(variables):
|
||||
yield from self._async_action(variables)
|
||||
yield from self._async_action(self.entity_id, variables)
|
||||
self._last_triggered = utcnow()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
def remove(self):
|
||||
@asyncio.coroutine
|
||||
def async_remove(self):
|
||||
"""Remove automation from HASS."""
|
||||
run_coroutine_threadsafe(self.async_turn_off(),
|
||||
self.hass.loop).result()
|
||||
super().remove()
|
||||
yield from self.async_turn_off()
|
||||
yield from super().async_remove()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_enable(self):
|
||||
@@ -341,12 +352,13 @@ def _async_process_config(hass, config, component):
|
||||
entity = AutomationEntity(name, async_attach_triggers, cond_func,
|
||||
action, hidden)
|
||||
if config_block[CONF_INITIAL_STATE]:
|
||||
tasks.append(hass.loop.create_task(entity.async_enable()))
|
||||
tasks.append(entity.async_enable())
|
||||
entities.append(entity)
|
||||
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, component.add_entities, entities)
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
return len(entities) > 0
|
||||
|
||||
@@ -356,11 +368,12 @@ def _async_get_action(hass, config, name):
|
||||
script_obj = script.Script(hass, config, name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def action(variables=None):
|
||||
def action(entity_id, variables):
|
||||
"""Action to be executed."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.async_log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
hass.loop.create_task(script_obj.async_run(variables))
|
||||
logbook.async_log_entry(
|
||||
hass, name, 'has been triggered', DOMAIN, entity_id)
|
||||
yield from script_obj.async_run(variables)
|
||||
|
||||
return action
|
||||
|
||||
@@ -393,9 +406,8 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
removes = []
|
||||
|
||||
for conf in trigger_configs:
|
||||
platform = yield from hass.loop.run_in_executor(
|
||||
None, prepare_setup_platform, hass, config, DOMAIN,
|
||||
conf.get(CONF_PLATFORM))
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
|
||||
|
||||
if platform is None:
|
||||
return None
|
||||
|
||||
98
homeassistant/components/automation/litejet.py
Normal file
98
homeassistant/components/automation/litejet.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Trigger an automation when a LiteJet switch is released.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/automation.litejet/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
|
||||
DEPENDENCIES = ['litejet']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NUMBER = 'number'
|
||||
CONF_HELD_MORE_THAN = 'held_more_than'
|
||||
CONF_HELD_LESS_THAN = 'held_less_than'
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'litejet',
|
||||
vol.Required(CONF_NUMBER): cv.positive_int,
|
||||
vol.Optional(CONF_HELD_MORE_THAN):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_HELD_LESS_THAN):
|
||||
vol.All(cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
|
||||
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
number = config.get(CONF_NUMBER)
|
||||
held_more_than = config.get(CONF_HELD_MORE_THAN)
|
||||
held_less_than = config.get(CONF_HELD_LESS_THAN)
|
||||
pressed_time = None
|
||||
cancel_pressed_more_than = None
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
CONF_PLATFORM: 'litejet',
|
||||
CONF_NUMBER: number,
|
||||
CONF_HELD_MORE_THAN: held_more_than,
|
||||
CONF_HELD_LESS_THAN: held_less_than
|
||||
},
|
||||
})
|
||||
|
||||
# held_more_than and held_less_than: trigger on released (if in time range)
|
||||
# held_more_than: trigger after pressed with calculation
|
||||
# held_less_than: trigger on released with calculation
|
||||
# neither: trigger on pressed
|
||||
|
||||
@callback
|
||||
def pressed_more_than_satisfied(now):
|
||||
"""Handle the LiteJet's switch's button pressed >= held_more_than."""
|
||||
call_action()
|
||||
|
||||
def pressed():
|
||||
"""Handle the press of the LiteJet switch's button."""
|
||||
nonlocal cancel_pressed_more_than, pressed_time
|
||||
nonlocal held_less_than, held_more_than
|
||||
pressed_time = dt_util.utcnow()
|
||||
if held_more_than is None and held_less_than is None:
|
||||
call_action()
|
||||
if held_more_than is not None and held_less_than is None:
|
||||
cancel_pressed_more_than = track_point_in_utc_time(
|
||||
hass,
|
||||
pressed_more_than_satisfied,
|
||||
dt_util.utcnow() + held_more_than)
|
||||
|
||||
def released():
|
||||
"""Handle the release of the LiteJet switch's button."""
|
||||
nonlocal cancel_pressed_more_than, pressed_time
|
||||
nonlocal held_less_than, held_more_than
|
||||
# pylint: disable=not-callable
|
||||
if cancel_pressed_more_than is not None:
|
||||
cancel_pressed_more_than()
|
||||
cancel_pressed_more_than = None
|
||||
held_time = dt_util.utcnow() - pressed_time
|
||||
if held_less_than is not None and held_time < held_less_than:
|
||||
if held_more_than is None or held_time > held_more_than:
|
||||
call_action()
|
||||
|
||||
hass.data['litejet_system'].on_switch_pressed(number, pressed)
|
||||
hass.data['litejet_system'].on_switch_released(number, released)
|
||||
|
||||
def async_remove():
|
||||
"""Remove all subscriptions used for this trigger."""
|
||||
return
|
||||
|
||||
return async_remove
|
||||
@@ -4,6 +4,8 @@ Offer MQTT listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||
"""
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
@@ -31,13 +33,20 @@ def async_trigger(hass, config, action):
|
||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||
"""Listen for MQTT messages."""
|
||||
if payload is None or payload == msg_payload:
|
||||
data = {
|
||||
'platform': 'mqtt',
|
||||
'topic': msg_topic,
|
||||
'payload': msg_payload,
|
||||
'qos': qos,
|
||||
}
|
||||
|
||||
try:
|
||||
data['payload_json'] = json.loads(msg_payload)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'mqtt',
|
||||
'topic': msg_topic,
|
||||
'payload': msg_payload,
|
||||
'qos': qos,
|
||||
}
|
||||
'trigger': data
|
||||
})
|
||||
|
||||
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
||||
|
||||
@@ -64,10 +64,19 @@ def async_trigger(hass, config, action):
|
||||
call_action()
|
||||
return
|
||||
|
||||
@callback
|
||||
def clear_listener():
|
||||
"""Clear all unsub listener."""
|
||||
nonlocal async_remove_state_for_cancel
|
||||
nonlocal async_remove_state_for_listener
|
||||
async_remove_state_for_listener = None
|
||||
async_remove_state_for_cancel = None
|
||||
|
||||
@callback
|
||||
def state_for_listener(now):
|
||||
"""Fire on state changes after a delay and calls action."""
|
||||
async_remove_state_for_cancel()
|
||||
clear_listener()
|
||||
call_action()
|
||||
|
||||
@callback
|
||||
@@ -77,6 +86,7 @@ def async_trigger(hass, config, action):
|
||||
return
|
||||
async_remove_state_for_listener()
|
||||
async_remove_state_for_cancel()
|
||||
clear_listener()
|
||||
|
||||
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||
|
||||
@@ -4,6 +4,7 @@ Component to interface with binary sensors.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -39,13 +40,13 @@ SENSOR_CLASSES = [
|
||||
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES))
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
sensor_class, pin)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
"""Implement an aREST binary sensor for a pin."""
|
||||
|
||||
@@ -93,7 +92,6 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
self.arest.update()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ArestData(object):
|
||||
"""Class for handling the data retrieval for pins."""
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.command_line/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -23,7 +22,7 @@ DEFAULT_NAME = 'Binary Command Sensor'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
SCAN_INTERVAL = 60
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COMMAND): cv.string,
|
||||
@@ -53,7 +52,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
value_template)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class CommandBinarySensor(BinarySensorDevice):
|
||||
"""Represent a command line binary sensor."""
|
||||
|
||||
|
||||
136
homeassistant/components/binary_sensor/concord232.py
Executable file
136
homeassistant/components/binary_sensor/concord232.py
Executable file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Support for exposing Concord232 elements as sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.concord232/
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_EXCLUDE_ZONES = 'exclude_zones'
|
||||
CONF_ZONE_TYPES = 'zone_types'
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'Alarm'
|
||||
DEFAULT_PORT = '5007'
|
||||
DEFAULT_SSL = False
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_EXCLUDE_ZONES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Concord232 binary sensor platform."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
exclude = config.get(CONF_EXCLUDE_ZONES)
|
||||
zone_types = config.get(CONF_ZONE_TYPES)
|
||||
sensors = []
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Initializing Client")
|
||||
client = concord232_client.Client('http://{}:{}'.format(host, port))
|
||||
client.zones = client.list_zones()
|
||||
client.last_zone_update = datetime.datetime.now()
|
||||
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return False
|
||||
|
||||
for zone in client.zones:
|
||||
_LOGGER.info("Loading Zone found: %s", zone['name'])
|
||||
if zone['number'] not in exclude:
|
||||
sensors.append(
|
||||
Concord232ZoneSensor(
|
||||
hass, client, zone, zone_types.get(zone['number'],
|
||||
get_opening_type(zone)))
|
||||
)
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_opening_type(zone):
|
||||
"""Helper function to try to guess sensor type from name."""
|
||||
if 'MOTION' in zone['name']:
|
||||
return 'motion'
|
||||
if 'KEY' in zone['name']:
|
||||
return 'safety'
|
||||
if 'SMOKE' in zone['name']:
|
||||
return 'smoke'
|
||||
if 'WATER' in zone['name']:
|
||||
return 'water'
|
||||
return 'opening'
|
||||
|
||||
|
||||
class Concord232ZoneSensor(BinarySensorDevice):
|
||||
"""Representation of a Concord232 zone as a sensor."""
|
||||
|
||||
def __init__(self, hass, client, zone, zone_type):
|
||||
"""Initialize the Concord232 binary sensor."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._zone = zone
|
||||
self._number = zone['number']
|
||||
self._zone_type = zone_type
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._zone['name']
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
# True means "faulted" or "open" or "abnormal state"
|
||||
return bool(self._zone['state'] == 'Normal')
|
||||
|
||||
def update(self):
|
||||
""""Get updated stats from API."""
|
||||
last_update = datetime.datetime.now() - self._client.last_zone_update
|
||||
_LOGGER.debug("Zone: %s ", self._zone)
|
||||
if last_update > datetime.timedelta(seconds=1):
|
||||
self._client.zones = self._client.list_zones()
|
||||
self._client.last_zone_update = datetime.datetime.now()
|
||||
_LOGGER.debug("Updated from Zone: %s", self._zone['name'])
|
||||
|
||||
if hasattr(self._client, 'zones'):
|
||||
self._zone = next((x for x in self._client.zones
|
||||
if x['number'] == self._number), None)
|
||||
@@ -20,7 +20,7 @@ DEPENDENCIES = ['enocean']
|
||||
DEFAULT_NAME = 'EnOcean binary sensor'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
@@ -35,7 +35,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""Representation of an Envisalink binary sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
|
||||
@@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
from haffmpeg import SensorNoise, SensorMotion
|
||||
|
||||
# check source
|
||||
if not run_test(config.get(CONF_INPUT)):
|
||||
if not run_test(hass, config.get(CONF_INPUT)):
|
||||
return
|
||||
|
||||
# generate sensor object
|
||||
@@ -138,7 +138,7 @@ class FFmpegBinarySensor(BinarySensorDevice):
|
||||
def _callback(self, state):
|
||||
"""HA-FFmpeg callback for noise detection."""
|
||||
self._state = state
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _start_ffmpeg(self, config):
|
||||
"""Start a FFmpeg instance."""
|
||||
|
||||
257
homeassistant/components/binary_sensor/flic.py
Normal file
257
homeassistant/components/binary_sensor/flic.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""Contains functionality to use flic buttons as a binary sensor."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, CONF_DISCOVERY, CONF_TIMEOUT,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
|
||||
REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TIMEOUT = 3
|
||||
|
||||
CLICK_TYPE_SINGLE = "single"
|
||||
CLICK_TYPE_DOUBLE = "double"
|
||||
CLICK_TYPE_HOLD = "hold"
|
||||
CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD]
|
||||
|
||||
CONF_IGNORED_CLICK_TYPES = "ignored_click_types"
|
||||
|
||||
EVENT_NAME = "flic_click"
|
||||
EVENT_DATA_NAME = "button_name"
|
||||
EVENT_DATA_ADDRESS = "button_address"
|
||||
EVENT_DATA_TYPE = "click_type"
|
||||
EVENT_DATA_QUEUED_TIME = "queued_time"
|
||||
|
||||
# Validation of the user's configuration
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||
vol.Optional(CONF_PORT, default=5551): cv.port,
|
||||
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All(cv.ensure_list,
|
||||
[vol.In(CLICK_TYPES)])
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Setup the flic platform."""
|
||||
import pyflic
|
||||
|
||||
# Initialize flic client responsible for
|
||||
# connecting to buttons and retrieving events
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
discovery = config.get(CONF_DISCOVERY)
|
||||
|
||||
try:
|
||||
client = pyflic.FlicClient(host, port)
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.error("Failed to connect to flic server.")
|
||||
return
|
||||
|
||||
def new_button_callback(address):
|
||||
"""Setup newly verified button as device in home assistant."""
|
||||
hass.add_job(async_setup_button(hass, config, async_add_entities,
|
||||
client, address))
|
||||
|
||||
client.on_new_verified_button = new_button_callback
|
||||
if discovery:
|
||||
start_scanning(hass, config, async_add_entities, client)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
lambda event: client.close())
|
||||
hass.loop.run_in_executor(None, client.handle_events)
|
||||
|
||||
# Get addresses of already verified buttons
|
||||
addresses = yield from async_get_verified_addresses(client)
|
||||
if addresses:
|
||||
for address in addresses:
|
||||
yield from async_setup_button(hass, config, async_add_entities,
|
||||
client, address)
|
||||
|
||||
|
||||
def start_scanning(hass, config, async_add_entities, client):
|
||||
"""Start a new flic client for scanning & connceting to new buttons."""
|
||||
import pyflic
|
||||
|
||||
scan_wizard = pyflic.ScanWizard()
|
||||
|
||||
def scan_completed_callback(scan_wizard, result, address, name):
|
||||
"""Restart scan wizard to constantly check for new buttons."""
|
||||
if result == pyflic.ScanWizardResult.WizardSuccess:
|
||||
_LOGGER.info("Found new button (%s)", address)
|
||||
elif result != pyflic.ScanWizardResult.WizardFailedTimeout:
|
||||
_LOGGER.warning("Failed to connect to button (%s). Reason: %s",
|
||||
address, result)
|
||||
|
||||
# Restart scan wizard
|
||||
start_scanning(hass, config, async_add_entities, client)
|
||||
|
||||
scan_wizard.on_completed = scan_completed_callback
|
||||
client.add_scan_wizard(scan_wizard)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_button(hass, config, async_add_entities, client, address):
|
||||
"""Setup single button device."""
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES)
|
||||
button = FlicButton(hass, client, address, timeout, ignored_click_types)
|
||||
_LOGGER.info("Connected to button (%s)", address)
|
||||
|
||||
yield from async_add_entities([button])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_verified_addresses(client):
|
||||
"""Retrieve addresses of verified buttons."""
|
||||
future = asyncio.Future()
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def get_info_callback(items):
|
||||
"""Set the addressed of connected buttons as result of the future."""
|
||||
addresses = items["bd_addr_of_verified_buttons"]
|
||||
run_callback_threadsafe(loop, future.set_result, addresses)
|
||||
client.get_info(get_info_callback)
|
||||
|
||||
return future
|
||||
|
||||
|
||||
class FlicButton(BinarySensorDevice):
|
||||
"""Representation of a flic button."""
|
||||
|
||||
def __init__(self, hass, client, address, timeout, ignored_click_types):
|
||||
"""Initialize the flic button."""
|
||||
import pyflic
|
||||
|
||||
self._hass = hass
|
||||
self._address = address
|
||||
self._timeout = timeout
|
||||
self._is_down = False
|
||||
self._ignored_click_types = ignored_click_types or []
|
||||
self._hass_click_types = {
|
||||
pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE,
|
||||
pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE,
|
||||
pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE,
|
||||
pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD,
|
||||
}
|
||||
|
||||
self._channel = self._create_channel()
|
||||
client.add_connection_channel(self._channel)
|
||||
|
||||
def _create_channel(self):
|
||||
"""Create a new connection channel to the button."""
|
||||
import pyflic
|
||||
|
||||
channel = pyflic.ButtonConnectionChannel(self._address)
|
||||
channel.on_button_up_or_down = self._on_up_down
|
||||
|
||||
# If all types of clicks should be ignored, skip registering callbacks
|
||||
if set(self._ignored_click_types) == set(CLICK_TYPES):
|
||||
return channel
|
||||
|
||||
if CLICK_TYPE_DOUBLE in self._ignored_click_types:
|
||||
# Listen to all but double click type events
|
||||
channel.on_button_click_or_hold = self._on_click
|
||||
elif CLICK_TYPE_HOLD in self._ignored_click_types:
|
||||
# Listen to all but hold click type events
|
||||
channel.on_button_single_or_double_click = self._on_click
|
||||
else:
|
||||
# Listen to all click type events
|
||||
channel.on_button_single_or_double_click_or_hold = self._on_click
|
||||
|
||||
return channel
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return "flic_%s" % self.address.replace(":", "")
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the bluetooth address of the device."""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._is_down
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = super(FlicButton, self).state_attributes
|
||||
attr["address"] = self.address
|
||||
|
||||
return attr
|
||||
|
||||
def _queued_event_check(self, click_type, time_diff):
|
||||
"""Generate a log message and returns true if timeout exceeded."""
|
||||
time_string = "{:d} {}".format(
|
||||
time_diff, "second" if time_diff == 1 else "seconds")
|
||||
|
||||
if time_diff > self._timeout:
|
||||
_LOGGER.warning(
|
||||
"Queued %s dropped for %s. Time in queue was %s.",
|
||||
click_type, self.address, time_string)
|
||||
return True
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s.",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
|
||||
def _on_up_down(self, channel, click_type, was_queued, time_diff):
|
||||
"""Update device state, if event was not queued."""
|
||||
import pyflic
|
||||
|
||||
if was_queued and self._queued_event_check(click_type, time_diff):
|
||||
return
|
||||
|
||||
self._is_down = click_type == pyflic.ClickType.ButtonDown
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _on_click(self, channel, click_type, was_queued, time_diff):
|
||||
"""Fire click event, if event was not queued."""
|
||||
# Return if click event was queued beyond allowed timeout
|
||||
if was_queued and self._queued_event_check(click_type, time_diff):
|
||||
return
|
||||
|
||||
# Return if click event is in ignored click types
|
||||
hass_click_type = self._hass_click_types[click_type]
|
||||
if hass_click_type in self._ignored_click_types:
|
||||
return
|
||||
|
||||
self._hass.bus.fire(EVENT_NAME, {
|
||||
EVENT_DATA_NAME: self.name,
|
||||
EVENT_DATA_ADDRESS: self.address,
|
||||
EVENT_DATA_QUEUED_TIME: time_diff,
|
||||
EVENT_DATA_TYPE: hass_click_type
|
||||
})
|
||||
|
||||
def _connection_status_changed(self, channel,
|
||||
connection_status, disconnect_reason):
|
||||
"""Remove device, if button disconnects."""
|
||||
import pyflic
|
||||
|
||||
if connection_status == pyflic.ConnectionStatus.Disconnected:
|
||||
_LOGGER.info("Button (%s) disconnected. Reason: %s",
|
||||
self.address, disconnect_reason)
|
||||
self.remove()
|
||||
262
homeassistant/components/binary_sensor/hikvision.py
Normal file
262
homeassistant/components/binary_sensor/hikvision.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Support for Hikvision event stream events represented as binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.hikvision/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.0.6', 'pydispatcher==2.0.5']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
CONF_DELAY = 'delay'
|
||||
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_IGNORED = False
|
||||
DEFAULT_DELAY = 0
|
||||
|
||||
ATTR_DELAY = 'delay'
|
||||
|
||||
SENSOR_CLASS_MAP = {
|
||||
'Motion': 'motion',
|
||||
'Line Crossing': 'motion',
|
||||
'IO Trigger': None,
|
||||
'Field Detection': 'motion',
|
||||
'Video Loss': None,
|
||||
'Tamper Detection': 'motion',
|
||||
'Shelter Alarm': None,
|
||||
'Disk Full': None,
|
||||
'Disk Error': None,
|
||||
'Net Interface Broken': 'connectivity',
|
||||
'IP Conflict': 'connectivity',
|
||||
'Illegal Access': None,
|
||||
'Video Mismatch': None,
|
||||
'Bad Video': None,
|
||||
'PIR Alarm': 'motion',
|
||||
'Face Detection': 'motion',
|
||||
}
|
||||
|
||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_IGNORED, default=DEFAULT_IGNORED): cv.boolean,
|
||||
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CUSTOMIZE, default={}):
|
||||
vol.Schema({cv.string: CUSTOMIZE_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup Hikvision binary sensor devices."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
customize = config.get(CONF_CUSTOMIZE)
|
||||
|
||||
if config.get(CONF_SSL):
|
||||
protocol = "https"
|
||||
else:
|
||||
protocol = "http"
|
||||
|
||||
url = '{}://{}'.format(protocol, host)
|
||||
|
||||
data = HikvisionData(hass, url, port, name, username, password)
|
||||
|
||||
if data.sensors is None:
|
||||
_LOGGER.error('Hikvision event stream has no data, unable to setup.')
|
||||
return False
|
||||
|
||||
entities = []
|
||||
|
||||
for sensor in data.sensors:
|
||||
# Build sensor name, then parse customize config.
|
||||
sensor_name = sensor.replace(' ', '_')
|
||||
|
||||
custom = customize.get(sensor_name.lower(), {})
|
||||
ignore = custom.get(CONF_IGNORED)
|
||||
delay = custom.get(CONF_DELAY)
|
||||
|
||||
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
|
||||
data.name, sensor_name, ignore, delay)
|
||||
if not ignore:
|
||||
entities.append(HikvisionBinarySensor(hass, sensor, data, delay))
|
||||
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class HikvisionData(object):
|
||||
"""Hikvision camera event stream object."""
|
||||
|
||||
def __init__(self, hass, url, port, name, username, password):
|
||||
"""Initialize the data oject."""
|
||||
from pyhik.hikvision import HikCamera
|
||||
self._url = url
|
||||
self._port = port
|
||||
self._name = name
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
# Establish camera
|
||||
self._cam = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
|
||||
if self._name is None:
|
||||
self._name = self._cam.get_name
|
||||
|
||||
# Start event stream
|
||||
self._cam.start_stream()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik)
|
||||
|
||||
def stop_hik(self, event):
|
||||
"""Shutdown Hikvision subscriptions and subscription thread on exit."""
|
||||
self._cam.disconnect()
|
||||
|
||||
@property
|
||||
def sensors(self):
|
||||
"""Return list of available sensors and their states."""
|
||||
return self._cam.current_event_states
|
||||
|
||||
@property
|
||||
def cam_id(self):
|
||||
"""Return camera id."""
|
||||
return self._cam.get_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return camera name."""
|
||||
return self._name
|
||||
|
||||
|
||||
class HikvisionBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Hikvision binary sensor."""
|
||||
|
||||
def __init__(self, hass, sensor, cam, delay):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
|
||||
self._hass = hass
|
||||
self._cam = cam
|
||||
self._name = self._cam.name + ' ' + sensor
|
||||
self._id = self._cam.cam_id + '.' + sensor
|
||||
self._sensor = sensor
|
||||
|
||||
if delay is None:
|
||||
self._delay = 0
|
||||
else:
|
||||
self._delay = delay
|
||||
|
||||
self._timer = None
|
||||
|
||||
# Form signal for dispatcher
|
||||
signal = 'ValueChanged.{}'.format(self._cam.cam_id)
|
||||
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=signal,
|
||||
sender=self._sensor)
|
||||
|
||||
def _sensor_state(self):
|
||||
"""Extract sensor state."""
|
||||
return self._cam.sensors[self._sensor][0]
|
||||
|
||||
def _sensor_last_update(self):
|
||||
"""Extract sensor last update time."""
|
||||
return self._cam.sensors[self._sensor][3]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Hikvision sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return '{}.{}'.format(self.__class__, self._id)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._sensor_state()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
try:
|
||||
return SENSOR_CLASS_MAP[self._sensor]
|
||||
except KeyError:
|
||||
# Sensor must be unknown to us, add as generic
|
||||
return None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr[ATTR_LAST_TRIP_TIME] = self._sensor_last_update()
|
||||
|
||||
if self._delay != 0:
|
||||
attr[ATTR_DELAY] = self._delay
|
||||
|
||||
return attr
|
||||
|
||||
def _update_callback(self, signal, sender):
|
||||
"""Update the sensor's state, if needed."""
|
||||
_LOGGER.debug('Dispatcher callback, signal: %s, sender: %s',
|
||||
signal, sender)
|
||||
|
||||
if sender is not self._sensor:
|
||||
return
|
||||
|
||||
if self._delay > 0 and not self.is_on:
|
||||
# Set timer to wait until updating the state
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
_LOGGER.debug('%s Called delayed (%ssec) update.',
|
||||
self._name, self._delay)
|
||||
self.schedule_update_ha_state()
|
||||
self._timer = None
|
||||
|
||||
if self._timer is not None:
|
||||
self._timer()
|
||||
self._timer = None
|
||||
|
||||
self._timer = track_point_in_utc_time(
|
||||
self._hass, _delay_update,
|
||||
utcnow() + timedelta(seconds=self._delay))
|
||||
|
||||
elif self._delay > 0 and self.is_on:
|
||||
# For delayed sensors kill any callbacks on true events and update
|
||||
if self._timer is not None:
|
||||
self._timer()
|
||||
self._timer = None
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
else:
|
||||
self.schedule_update_ha_state()
|
||||
@@ -7,7 +7,8 @@ https://home-assistant.io/components/binary_sensor.homematic/
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,14 +33,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMBinarySensor,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
|
||||
|
||||
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
|
||||
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
"""Representation of a binary Homematic device."""
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES)
|
||||
@@ -51,7 +52,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttBinarySensor(BinarySensorDevice):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
@@ -67,17 +67,18 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
self._payload_off = payload_off
|
||||
self._qos = qos
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._payload_on:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._payload_off:
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
if not gateways:
|
||||
return
|
||||
|
||||
for gateway in gateways:
|
||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||
# states. Map them in a dict of lists.
|
||||
pres = gateway.const.Presentation
|
||||
|
||||
@@ -4,40 +4,100 @@ Support for Nest Thermostat Binary Sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.nest/
|
||||
"""
|
||||
from itertools import chain
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
|
||||
import homeassistant.components.nest as nest
|
||||
from homeassistant.components.nest import (
|
||||
DATA_NEST, is_thermostat, is_camera)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
BINARY_TYPES = ['fan',
|
||||
'hvac_ac_state',
|
||||
'hvac_aux_heater_state',
|
||||
'hvac_heater_state',
|
||||
'hvac_heat_x2_state',
|
||||
'hvac_heat_x3_state',
|
||||
'hvac_alt_heat_state',
|
||||
'hvac_alt_heat_x2_state',
|
||||
'hvac_emer_heat_state',
|
||||
'online']
|
||||
|
||||
BINARY_TYPES = ['online']
|
||||
|
||||
CLIMATE_BINARY_TYPES = ['fan',
|
||||
'is_using_emergency_heat',
|
||||
'is_locked',
|
||||
'has_leaf']
|
||||
|
||||
CAMERA_BINARY_TYPES = [
|
||||
'motion_detected',
|
||||
'sound_detected',
|
||||
'person_detected']
|
||||
|
||||
_BINARY_TYPES_DEPRECATED = [
|
||||
'hvac_ac_state',
|
||||
'hvac_aux_heater_state',
|
||||
'hvac_heater_state',
|
||||
'hvac_heat_x2_state',
|
||||
'hvac_heat_x3_state',
|
||||
'hvac_alt_heat_state',
|
||||
'hvac_alt_heat_x2_state',
|
||||
'hvac_emer_heat_state']
|
||||
|
||||
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \
|
||||
+ CAMERA_BINARY_TYPES
|
||||
_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED = _VALID_BINARY_SENSOR_TYPES \
|
||||
+ _BINARY_TYPES_DEPRECATED
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_INTERVAL):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Required(CONF_MONITORED_CONDITIONS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_TYPES)]),
|
||||
vol.All(cv.ensure_list,
|
||||
[vol.In(_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED)])
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Nest binary sensors."""
|
||||
for structure, device in nest.devices():
|
||||
add_devices([NestBinarySensor(structure, device, variable)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]])
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
nest = hass.data[DATA_NEST]
|
||||
conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_BINARY_SENSOR_TYPES)
|
||||
|
||||
for variable in conf:
|
||||
if variable in _BINARY_TYPES_DEPRECATED:
|
||||
wstr = (variable + " is no a longer supported "
|
||||
"monitored_conditions. See "
|
||||
"https://home-assistant.io/components/binary_sensor.nest/ "
|
||||
"for valid options, or remove monitored_conditions "
|
||||
"entirely to get a reasonable default")
|
||||
_LOGGER.error(wstr)
|
||||
|
||||
sensors = []
|
||||
device_chain = chain(nest.devices(),
|
||||
nest.protect_devices(),
|
||||
nest.camera_devices())
|
||||
for structure, device in device_chain:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
if variable in BINARY_TYPES]
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
if variable in CLIMATE_BINARY_TYPES
|
||||
and is_thermostat(device)]
|
||||
|
||||
if is_camera(device):
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
if variable in CAMERA_BINARY_TYPES]
|
||||
for activity_zone in device.activity_zones:
|
||||
sensors += [NestActivityZoneSensor(structure,
|
||||
device,
|
||||
activity_zone)]
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
@@ -46,4 +106,26 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the binary sensor is on."""
|
||||
return bool(getattr(self.device, self.variable))
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = bool(getattr(self.device, self.variable))
|
||||
|
||||
|
||||
class NestActivityZoneSensor(NestBinarySensor):
|
||||
"""Represents a Nest binary sensor for activity in a zone."""
|
||||
|
||||
def __init__(self, structure, device, zone):
|
||||
"""Initialize the sensor."""
|
||||
super(NestActivityZoneSensor, self).__init__(structure, device, None)
|
||||
self.zone = zone
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return "{} {} activity".format(self._name, self.zone.name)
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id)
|
||||
|
||||
160
homeassistant/components/binary_sensor/netatmo.py
Normal file
160
homeassistant/components/binary_sensor/netatmo.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Support for the Netatmo binary sensors.
|
||||
|
||||
The binary sensors based on events seen by the NetatmoCamera
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.netatmo/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.netatmo import WelcomeData
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_TIMEOUT
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
"Someone known": 'occupancy',
|
||||
"Someone unknown": 'motion',
|
||||
"Motion": 'motion',
|
||||
"Tag Vibration": 'vibration',
|
||||
"Tag Open": 'opening',
|
||||
}
|
||||
|
||||
CONF_HOME = 'home'
|
||||
CONF_CAMERAS = 'cameras'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup access to Netatmo binary sensor."""
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME, None)
|
||||
timeout = config.get(CONF_TIMEOUT, 15)
|
||||
|
||||
module_name = None
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
if data.get_camera_names() == []:
|
||||
return None
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES)
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
for variable in sensors:
|
||||
if variable in ('Tag Vibration', 'Tag Open'):
|
||||
continue
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, module_name,
|
||||
home, timeout, variable)])
|
||||
|
||||
for module_name in data.get_module_names(camera_name):
|
||||
for variable in sensors:
|
||||
if variable in ('Tag Vibration', 'Tag Open'):
|
||||
add_devices([WelcomeBinarySensor(data, camera_name,
|
||||
module_name, home,
|
||||
timeout, variable)])
|
||||
|
||||
|
||||
class WelcomeBinarySensor(BinarySensorDevice):
|
||||
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
||||
|
||||
def __init__(self, data, camera_name, module_name, home, timeout, sensor):
|
||||
"""Setup for access to the Netatmo camera events."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._module_name = module_name
|
||||
self._home = home
|
||||
self._timeout = timeout
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
if module_name:
|
||||
self._name += ' / ' + module_name
|
||||
self._sensor_name = sensor
|
||||
self._name += ' ' + sensor
|
||||
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
||||
home=home)['id']
|
||||
self._unique_id = "Welcome_binary_sensor {0} - {1}".format(self._name,
|
||||
camera_id)
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the Netatmo device and this sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Request an update from the Netatmo API."""
|
||||
self._data.update()
|
||||
self._data.update_event()
|
||||
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneKnownSeen(self._home,
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Someone unknown":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneUnknownSeen(self._home,
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Motion":
|
||||
self._state =\
|
||||
self._data.welcomedata.motionDetected(self._home,
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Tag Vibration":
|
||||
self._state =\
|
||||
self._data.welcomedata.moduleMotionDetected(self._home,
|
||||
self._module_name,
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Tag Open":
|
||||
self._state =\
|
||||
self._data.welcomedata.moduleOpened(self._home,
|
||||
self._module_name,
|
||||
self._camera_name)
|
||||
else:
|
||||
return None
|
||||
@@ -123,7 +123,7 @@ class NX584Watcher(threading.Thread):
|
||||
if not zone_sensor:
|
||||
return
|
||||
zone_sensor._zone['state'] = event['zone_state']
|
||||
zone_sensor.update_ha_state()
|
||||
zone_sensor.schedule_update_ha_state()
|
||||
|
||||
def _process_events(self, events):
|
||||
for event in events:
|
||||
|
||||
@@ -58,11 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class OctoPrintBinarySensor(BinarySensorDevice):
|
||||
"""Representation an OctoPrint binary sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, api, condition, sensor_type, sensor_name, unit,
|
||||
endpoint, group, tool=None):
|
||||
"""Initialize a new OctoPrint sensor."""
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
@@ -30,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_AUTHENTICATION):
|
||||
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
|
||||
vol.Optional(CONF_HEADERS): cv.string,
|
||||
vol.Optional(CONF_HEADERS): {cv.string: cv.string},
|
||||
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
@@ -42,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-variable, too-many-locals
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the REST binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
@@ -52,7 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
headers = json.loads(config.get(CONF_HEADERS, '{}'))
|
||||
headers = config.get(CONF_HEADERS)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
@@ -70,14 +68,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
rest.update()
|
||||
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch REST data')
|
||||
_LOGGER.error("Unable to fetch REST data from %s", resource)
|
||||
return False
|
||||
|
||||
add_devices([RestBinarySensor(
|
||||
hass, rest, name, sensor_class, value_template)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a REST binary sensor."""
|
||||
|
||||
@@ -109,8 +106,8 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
if self._value_template is not None:
|
||||
response = self._value_template.render_with_possible_json_value(
|
||||
self.rest.data, False)
|
||||
response = self._value_template.\
|
||||
async_render_with_possible_json_value(self.rest.data, False)
|
||||
|
||||
try:
|
||||
return bool(int(response))
|
||||
|
||||
@@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(binary_sensors)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
"""Represent a binary sensor that uses Raspberry Pi GPIO."""
|
||||
|
||||
@@ -73,7 +72,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
def read_gpio(port):
|
||||
"""Read state from GPIO."""
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
|
||||
"""Implementation of a SleepIQ presence sensor."""
|
||||
|
||||
|
||||
@@ -7,21 +7,20 @@ https://home-assistant.io/components/binary_sensor.tcp/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.tcp import Sensor, CONF_VALUE_ON
|
||||
|
||||
from homeassistant.components.sensor.tcp import (
|
||||
TcpSensor, CONF_VALUE_ON, PLATFORM_SCHEMA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Create the binary sensor."""
|
||||
if not BinarySensor.validate_config(config):
|
||||
return False
|
||||
|
||||
add_entities((BinarySensor(hass, config),))
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
|
||||
|
||||
|
||||
class BinarySensor(BinarySensorDevice, Sensor):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the TCP binary sensor."""
|
||||
add_devices([TcpBinarySensor(hass, config)])
|
||||
|
||||
|
||||
class TcpBinarySensor(BinarySensorDevice, TcpSensor):
|
||||
"""A binary sensor which is on when its state == CONF_VALUE_ON."""
|
||||
|
||||
required = (CONF_VALUE_ON,)
|
||||
|
||||
@@ -17,8 +17,8 @@ from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,7 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup template binary sensors."""
|
||||
sensors = []
|
||||
|
||||
@@ -61,34 +62,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if not sensors:
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
add_devices(sensors)
|
||||
|
||||
yield from async_add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
class BinarySensorTemplate(BinarySensorDevice):
|
||||
"""A virtual binary sensor that triggers from another sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, device, friendly_name, sensor_class,
|
||||
value_template, entity_ids):
|
||||
"""Initialize the Template binary sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
hass=hass)
|
||||
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
hass=hass)
|
||||
self._name = friendly_name
|
||||
self._sensor_class = sensor_class
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
|
||||
self.update()
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
hass.loop.create_task(self.async_update_ha_state(True))
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
track_state_change(hass, entity_ids, template_bsensor_state_listener)
|
||||
async_track_state_change(
|
||||
hass, entity_ids, template_bsensor_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -112,7 +111,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
"""Update the state from the template."""
|
||||
try:
|
||||
self._state = self._template.async_render().lower() == 'true'
|
||||
except TemplateError as ex:
|
||||
|
||||
128
homeassistant/components/binary_sensor/threshold.py
Normal file
128
homeassistant/components/binary_sensor/threshold.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Support for monitoring if a sensor value is below/above a threshold.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.threshold/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, CONF_SENSOR_CLASS,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_SENSOR_VALUE = 'sensor_value'
|
||||
ATTR_THRESHOLD = 'threshold'
|
||||
ATTR_TYPE = 'type'
|
||||
|
||||
CONF_LOWER = 'lower'
|
||||
CONF_THRESHOLD = 'threshold'
|
||||
CONF_UPPER = 'upper'
|
||||
|
||||
DEFAULT_NAME = 'Threshold'
|
||||
|
||||
SENSOR_TYPES = [CONF_LOWER, CONF_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_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
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)
|
||||
limit_type = config.get(CONF_TYPE)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
|
||||
yield from async_add_devices(
|
||||
[ThresholdSensor(hass, entity_id, name, threshold, limit_type,
|
||||
sensor_class)], True)
|
||||
return True
|
||||
|
||||
|
||||
class ThresholdSensor(BinarySensorDevice):
|
||||
"""Representation of a Threshold sensor."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, threshold, limit_type,
|
||||
sensor_class):
|
||||
"""Initialize the Threshold sensor."""
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self.is_upper = limit_type == 'upper'
|
||||
self._name = name
|
||||
self._threshold = threshold
|
||||
self._sensor_class = sensor_class
|
||||
self._deviation = False
|
||||
self.sensor_value = 0
|
||||
|
||||
@callback
|
||||
# pylint: disable=invalid-name
|
||||
def async_threshold_sensor_state_listener(
|
||||
entity, old_state, new_state):
|
||||
"""Called when the sensor changes state."""
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
return
|
||||
|
||||
try:
|
||||
self.sensor_value = float(new_state.state)
|
||||
except ValueError:
|
||||
_LOGGER.error("State is not numerical")
|
||||
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
async_track_state_change(
|
||||
hass, entity_id, async_threshold_sensor_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._deviation
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._sensor_class
|
||||
|
||||
@property
|
||||
def 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_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
if self.is_upper:
|
||||
self._deviation = bool(self.sensor_value > self._threshold)
|
||||
else:
|
||||
self._deviation = bool(self.sensor_value < self._threshold)
|
||||
@@ -4,8 +4,11 @@ A sensor that monitors trands in other components.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.trend/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -72,7 +75,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class SensorTrend(BinarySensorDevice):
|
||||
"""Representation of a trend Sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
def __init__(self, hass, device_id, friendly_name,
|
||||
target_entity, attribute, sensor_class, invert):
|
||||
"""Initialize the sensor."""
|
||||
@@ -88,13 +90,12 @@ class SensorTrend(BinarySensorDevice):
|
||||
self.from_state = None
|
||||
self.to_state = None
|
||||
|
||||
self.update()
|
||||
|
||||
@callback
|
||||
def trend_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.from_state = old_state
|
||||
self.to_state = new_state
|
||||
self.update_ha_state(True)
|
||||
hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
track_state_change(hass, target_entity,
|
||||
trend_sensor_state_listener)
|
||||
@@ -119,7 +120,8 @@ class SensorTrend(BinarySensorDevice):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
if self.from_state is None or self.to_state is None:
|
||||
return
|
||||
|
||||
@@ -16,9 +16,9 @@ DEPENDENCIES = ['vera']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Vera controller devices."""
|
||||
add_devices_callback(
|
||||
add_devices(
|
||||
VeraBinarySensor(device, VERA_CONTROLLER)
|
||||
for device in VERA_DEVICES['binary_sensor'])
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ class WemoBinarySensor(BinarySensorDevice):
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s',
|
||||
_device)
|
||||
self.update()
|
||||
if not hasattr(self, 'hass'):
|
||||
self.update()
|
||||
return
|
||||
self.update_ha_state(True)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Wink binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/binary_sensor.wink/
|
||||
"""
|
||||
import json
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
@@ -33,33 +32,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
for sensor in pywink.get_sensors():
|
||||
if sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
|
||||
for key in pywink.get_keys():
|
||||
add_devices([WinkBinarySensorDevice(key)])
|
||||
add_devices([WinkBinarySensorDevice(key, hass)])
|
||||
|
||||
for sensor in pywink.get_smoke_and_co_detectors():
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
|
||||
for hub in pywink.get_hubs():
|
||||
add_devices([WinkHub(hub, hass)])
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink)
|
||||
super().__init__(wink, hass)
|
||||
wink = get_component('wink')
|
||||
self._unit_of_measurement = self.wink.UNIT
|
||||
self.capability = self.wink.capability()
|
||||
|
||||
def _pubnub_update(self, message, channel):
|
||||
if 'data' in message:
|
||||
json_data = json.dumps(message.get('data'))
|
||||
else:
|
||||
json_data = message
|
||||
self.wink.pubnub_update(json.loads(json_data))
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
@@ -88,3 +82,24 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
|
||||
class WinkHub(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink Hub."""
|
||||
|
||||
def __init(self, wink, hass):
|
||||
"""Initialize the hub sensor."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'update needed': self.wink.update_needed(),
|
||||
'firmware version': self.wink.firmware_version()
|
||||
}
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.wink.state()
|
||||
|
||||
@@ -96,7 +96,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
||||
@@ -112,19 +112,19 @@ class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
||||
# If it's active make sure that we set the timeout tracker
|
||||
if sensor_value.data:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
if value.data:
|
||||
# only allow this value to be true for re_arm secs
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@property
|
||||
|
||||
@@ -33,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument,too-few-public-methods
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Setup BloomSky component."""
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
|
||||
183
homeassistant/components/calendar/__init__.py
Normal file
183
homeassistant/components/calendar/__init__.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Support for Google Calendar event device sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar/
|
||||
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from homeassistant.components.google import (CONF_OFFSET,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_NAME)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.helpers.config_validation import time_period_str
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.util import dt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'calendar'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
DEFAULT_CONF_TRACK_NEW = True
|
||||
DEFAULT_CONF_OFFSET = '!!'
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class CalendarEventDevice(Entity):
|
||||
"""A calendar event device."""
|
||||
|
||||
# Classes overloading this must set data to an object
|
||||
# with an update() method
|
||||
data = None
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, data):
|
||||
"""Create the Calendar Event Device."""
|
||||
self._name = data.get(CONF_NAME)
|
||||
self.dev_id = data.get(CONF_DEVICE_ID)
|
||||
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT,
|
||||
self.dev_id,
|
||||
hass=hass)
|
||||
|
||||
self._cal_data = {
|
||||
'all_day': False,
|
||||
'offset_time': dt.dt.timedelta(),
|
||||
'message': '',
|
||||
'start': None,
|
||||
'end': None,
|
||||
'location': '',
|
||||
'description': '',
|
||||
}
|
||||
|
||||
self.update()
|
||||
|
||||
def offset_reached(self):
|
||||
"""Have we reached the offset time specified in the event title."""
|
||||
if self._cal_data['start'] is None or \
|
||||
self._cal_data['offset_time'] == dt.dt.timedelta():
|
||||
return False
|
||||
|
||||
return self._cal_data['start'] + self._cal_data['offset_time'] <= \
|
||||
dt.now(self._cal_data['start'].tzinfo)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""State Attributes for HA."""
|
||||
start = self._cal_data.get('start', None)
|
||||
end = self._cal_data.get('end', None)
|
||||
start = start.strftime(DATE_STR_FORMAT) if start is not None else None
|
||||
end = end.strftime(DATE_STR_FORMAT) if end is not None else None
|
||||
|
||||
return {
|
||||
'message': self._cal_data.get('message', ''),
|
||||
'all_day': self._cal_data.get('all_day', False),
|
||||
'offset_reached': self.offset_reached(),
|
||||
'start_time': start,
|
||||
'end_time': end,
|
||||
'location': self._cal_data.get('location', None),
|
||||
'description': self._cal_data.get('description', None),
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the calendar event."""
|
||||
start = self._cal_data.get('start', None)
|
||||
end = self._cal_data.get('end', None)
|
||||
if start is None or end is None:
|
||||
return STATE_OFF
|
||||
|
||||
now = dt.now()
|
||||
|
||||
if start <= now and end > now:
|
||||
return STATE_ON
|
||||
|
||||
if now >= end:
|
||||
self.cleanup()
|
||||
|
||||
return STATE_OFF
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup any start/end listeners that were setup."""
|
||||
self._cal_data = {
|
||||
'all_day': False,
|
||||
'offset_time': 0,
|
||||
'message': '',
|
||||
'start': None,
|
||||
'end': None,
|
||||
'location': None,
|
||||
'description': None
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Search for the next event."""
|
||||
if not self.data or not self.data.update():
|
||||
# update cached, don't do anything
|
||||
return
|
||||
|
||||
if not self.data.event:
|
||||
# we have no event to work on, make sure we're clean
|
||||
self.cleanup()
|
||||
return
|
||||
|
||||
def _get_date(date):
|
||||
"""Get the dateTime from date or dateTime as a local."""
|
||||
if 'date' in date:
|
||||
return dt.as_utc(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time()))
|
||||
else:
|
||||
return dt.parse_datetime(date['dateTime'])
|
||||
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
||||
summary = self.data.event['summary']
|
||||
|
||||
# check if we have an offset tag in the message
|
||||
# time is HH:MM or MM
|
||||
reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(self._offset)
|
||||
search = re.search(reg, summary)
|
||||
if search and search.group(1):
|
||||
time = search.group(1)
|
||||
if ':' not in time:
|
||||
if time[0] == '+' or time[0] == '-':
|
||||
time = '{}0:{}'.format(time[0], time[1:])
|
||||
else:
|
||||
time = '0:{}'.format(time)
|
||||
|
||||
offset_time = time_period_str(time)
|
||||
summary = (summary[:search.start()] + summary[search.end():]) \
|
||||
.strip()
|
||||
else:
|
||||
offset_time = dt.dt.timedelta() # default it
|
||||
|
||||
# cleanup the string so we don't have a bunch of double+ spaces
|
||||
self._cal_data['message'] = re.sub(' +', '', summary).strip()
|
||||
|
||||
self._cal_data['offset_time'] = offset_time
|
||||
self._cal_data['location'] = self.data.event.get('location', '')
|
||||
self._cal_data['description'] = self.data.event.get('description', '')
|
||||
self._cal_data['start'] = start
|
||||
self._cal_data['end'] = end
|
||||
self._cal_data['all_day'] = 'date' in self.data.event['start']
|
||||
82
homeassistant/components/calendar/demo.py
Executable file
82
homeassistant/components/calendar/demo.py
Executable file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Demo platform that has two fake binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo binary sensor platform."""
|
||||
calendar_data_future = DemoGoogleCalendarDataFuture()
|
||||
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
||||
add_devices([
|
||||
DemoGoogleCalendar(hass, calendar_data_future, {
|
||||
CONF_NAME: 'Future Event',
|
||||
CONF_DEVICE_ID: 'future_event',
|
||||
}),
|
||||
|
||||
DemoGoogleCalendar(hass, calendar_data_current, {
|
||||
CONF_NAME: 'Current Event',
|
||||
CONF_DEVICE_ID: 'current_event',
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
class DemoGoogleCalendarData(object):
|
||||
"""Setup base class for data."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def update(self):
|
||||
"""Return true so entity knows we have new data."""
|
||||
return True
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
||||
"""Setup future data event."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event to a future event."""
|
||||
one_hour_from_now = dt_util.now() \
|
||||
+ dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': one_hour_from_now.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (one_hour_from_now + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Future Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
|
||||
"""Create a current event we're in the middle of."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event data."""
|
||||
middle_of_event = dt_util.now() \
|
||||
- dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': middle_of_event.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (middle_of_event + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Current Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendar(CalendarEventDevice):
|
||||
"""A Demo binary sensor."""
|
||||
|
||||
def __init__(self, hass, calendar_data, data):
|
||||
"""The same as a google calendar but without the api calls."""
|
||||
self.data = calendar_data
|
||||
super().__init__(hass, data)
|
||||
79
homeassistant/components/calendar/google.py
Normal file
79
homeassistant/components/calendar/google.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Support for Google Calendar Search binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.google_calendar/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import (CONF_CAL_ID, CONF_ENTITIES,
|
||||
CONF_TRACK, TOKEN_FILE,
|
||||
GoogleCalendarService)
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
||||
'orderBy': 'startTime',
|
||||
'maxResults': 1,
|
||||
'singleEvents': True,
|
||||
}
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
"""Setup the calendar platform for event devices."""
|
||||
if disc_info is None:
|
||||
return
|
||||
|
||||
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
||||
return
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
|
||||
disc_info[CONF_CAL_ID], data)
|
||||
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
"""A calendar event device."""
|
||||
|
||||
def __init__(self, hass, calendar_service, calendar, data):
|
||||
"""Create the Calendar event device."""
|
||||
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||
data.get('search', None))
|
||||
super().__init__(hass, data)
|
||||
|
||||
|
||||
class GoogleCalendarData(object):
|
||||
"""Class to utilize calendar service object to get next event."""
|
||||
|
||||
def __init__(self, calendar_service, calendar_id, search=None):
|
||||
"""Setup how we are going to search the google calendar."""
|
||||
self.calendar_service = calendar_service
|
||||
self.calendar_id = calendar_id
|
||||
self.search = search
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.utcnow().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
if self.search:
|
||||
params['q'] = self.search
|
||||
|
||||
events = service.events() # pylint: disable=no-member
|
||||
result = events.list(**params).execute()
|
||||
|
||||
items = result.get('items', [])
|
||||
self.event = items[0] if len(items) == 1 else None
|
||||
return True
|
||||
@@ -5,13 +5,15 @@ Component to interface with cameras.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -25,17 +27,16 @@ STATE_IDLE = 'idle'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the camera component."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.wsgi.register_view(CameraImageView(hass, component.entities))
|
||||
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
|
||||
|
||||
component.setup(config)
|
||||
hass.http.register_view(CameraImageView(component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(component.entities))
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
@@ -80,33 +81,58 @@ class Camera(Entity):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
def stream():
|
||||
"""Stream images as mjpeg stream."""
|
||||
try:
|
||||
last_image = None
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return bytes of camera image.
|
||||
|
||||
if img_bytes is not None and img_bytes != last_image:
|
||||
yield bytes(
|
||||
'--jpegboundary\r\n'
|
||||
'Content-Type: image/jpeg\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n'
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
image = yield from self.hass.loop.run_in_executor(
|
||||
None, self.camera_image)
|
||||
return image
|
||||
|
||||
last_image = img_bytes
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from camera images.
|
||||
|
||||
time.sleep(0.5)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
response = web.StreamResponse()
|
||||
|
||||
return response(
|
||||
stream(),
|
||||
content_type=('multipart/x-mixed-replace; '
|
||||
'boundary=--jpegboundary')
|
||||
)
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--jpegboundary')
|
||||
yield from response.prepare(request)
|
||||
|
||||
def write(img_bytes):
|
||||
"""Write image to stream."""
|
||||
response.write(bytes(
|
||||
'--jpegboundary\r\n'
|
||||
'Content-Type: image/jpeg\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
|
||||
|
||||
last_image = None
|
||||
|
||||
try:
|
||||
while True:
|
||||
img_bytes = yield from self.async_camera_image()
|
||||
if not img_bytes:
|
||||
break
|
||||
|
||||
if img_bytes is not None and img_bytes != last_image:
|
||||
write(img_bytes)
|
||||
|
||||
# Chrome seems to always ignore first picture,
|
||||
# print it twice.
|
||||
if last_image is None:
|
||||
write(img_bytes)
|
||||
|
||||
last_image = img_bytes
|
||||
yield from response.drain()
|
||||
|
||||
yield from asyncio.sleep(.5)
|
||||
finally:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -139,27 +165,29 @@ class CameraView(HomeAssistantView):
|
||||
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass, entities):
|
||||
def __init__(self, entities):
|
||||
"""Initialize a basic camera view."""
|
||||
super().__init__(hass)
|
||||
self.entities = entities
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, entity_id):
|
||||
"""Start a get request."""
|
||||
camera = self.entities.get(entity_id)
|
||||
|
||||
if camera is None:
|
||||
return self.Response(status=404)
|
||||
return web.Response(status=404)
|
||||
|
||||
authenticated = (request.authenticated or
|
||||
request.args.get('token') == camera.access_token)
|
||||
authenticated = (request[KEY_AUTHENTICATED] or
|
||||
request.GET.get('token') == camera.access_token)
|
||||
|
||||
if not authenticated:
|
||||
return self.Response(status=401)
|
||||
return web.Response(status=401)
|
||||
|
||||
return self.handle(camera)
|
||||
response = yield from self.handle(request, camera)
|
||||
return response
|
||||
|
||||
def handle(self, camera):
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Hanlde the camera request."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -167,25 +195,27 @@ class CameraView(HomeAssistantView):
|
||||
class CameraImageView(CameraView):
|
||||
"""Camera view to serve an image."""
|
||||
|
||||
url = "/api/camera_proxy/<entity(domain=camera):entity_id>"
|
||||
url = "/api/camera_proxy/{entity_id}"
|
||||
name = "api:camera:image"
|
||||
|
||||
def handle(self, camera):
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
response = camera.camera_image()
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
if response is None:
|
||||
return self.Response(status=500)
|
||||
if image is None:
|
||||
return web.Response(status=500)
|
||||
|
||||
return self.Response(response)
|
||||
return web.Response(body=image)
|
||||
|
||||
|
||||
class CameraMjpegStream(CameraView):
|
||||
"""Camera View to serve an MJPEG stream."""
|
||||
|
||||
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>"
|
||||
url = "/api/camera_proxy_stream/{entity_id}"
|
||||
name = "api:camera:stream"
|
||||
|
||||
def handle(self, camera):
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
return camera.mjpeg_stream(self.Response)
|
||||
yield from camera.handle_async_mjpeg_stream(request)
|
||||
|
||||
90
homeassistant/components/camera/amcrest.py
Normal file
90
homeassistant/components/camera/amcrest.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
This component provides basic support for Amcrest IP cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.amcrest/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
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 an Amcrest IP Camera."""
|
||||
from amcrest import AmcrestCamera
|
||||
data = AmcrestCamera(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME), config.get(CONF_PASSWORD))
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
data.camera.current_time
|
||||
# pylint: disable=broad-except
|
||||
except Exception as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
add_devices([AmcrestCam(config, data)])
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, device_info, data):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._data = data
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = self._data.camera.snapshot(channel=self._resolution)
|
||||
return response.data
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
@@ -4,15 +4,18 @@ Support for Cameras with FFmpeg as decoder.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.ffmpeg/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import (
|
||||
run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
||||
async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
@@ -27,17 +30,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
if not run_test(config.get(CONF_INPUT)):
|
||||
if not async_run_test(hass, config.get(CONF_INPUT)):
|
||||
return
|
||||
add_devices([FFmpegCamera(config)])
|
||||
yield from async_add_devices([FFmpegCamera(hass, config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
"""An implementation of an FFmpeg camera."""
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a FFmpeg camera."""
|
||||
super().__init__()
|
||||
self._name = config.get(CONF_NAME)
|
||||
@@ -45,24 +49,44 @@ class FFmpegCamera(Camera):
|
||||
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageSingle, IMAGE_JPEG
|
||||
ffmpeg = ImageSingle(get_binary())
|
||||
from haffmpeg import ImageSingleAsync, IMAGE_JPEG
|
||||
ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop)
|
||||
|
||||
return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments)
|
||||
image = yield from ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments)
|
||||
return image
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
from haffmpeg import CameraMjpegAsync
|
||||
|
||||
stream = CameraMjpeg(get_binary())
|
||||
stream.open_camera(self._input, extra_cmd=self._extra_arguments)
|
||||
return response(
|
||||
stream,
|
||||
mimetype='multipart/x-mixed-replace;boundary=ffserver',
|
||||
direct_passthrough=True
|
||||
)
|
||||
stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
self._input, extra_cmd=self._extra_arguments)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = 'multipart/x-mixed-replace;boundary=ffserver'
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
finally:
|
||||
self.hass.async_add_job(stream.close())
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([FoscamCamera(config)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class FoscamCamera(Camera):
|
||||
"""An implementation of a Foscam IP camera."""
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@ Support for IP Cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.generic/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
from requests.auth import HTTPDigestAuth
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -15,7 +18,9 @@ from homeassistant.const import (
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,13 +40,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a generic IP Camera."""
|
||||
add_devices([GenericCamera(hass, config)])
|
||||
yield from async_add_devices([GenericCamera(hass, config)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GenericCamera(Camera):
|
||||
"""A generic implementation of an IP camera."""
|
||||
|
||||
@@ -49,6 +54,7 @@ class GenericCamera(Camera):
|
||||
"""Initialize a generic camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._authentication = device_info.get(CONF_AUTHENTICATION)
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
||||
self._still_image_url.hass = hass
|
||||
@@ -58,10 +64,10 @@ class GenericCamera(Camera):
|
||||
password = device_info.get(CONF_PASSWORD)
|
||||
|
||||
if username and password:
|
||||
if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION:
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
self._auth = HTTPDigestAuth(username, password)
|
||||
else:
|
||||
self._auth = HTTPBasicAuth(username, password)
|
||||
self._auth = aiohttp.BasicAuth(username, password=password)
|
||||
else:
|
||||
self._auth = None
|
||||
|
||||
@@ -69,9 +75,15 @@ class GenericCamera(Camera):
|
||||
self._last_image = None
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
try:
|
||||
url = self._still_image_url.render()
|
||||
url = self._still_image_url.async_render()
|
||||
except TemplateError as err:
|
||||
_LOGGER.error('Error parsing template %s: %s',
|
||||
self._still_image_url, err)
|
||||
@@ -80,16 +92,40 @@ class GenericCamera(Camera):
|
||||
if url == self._last_url and self._limit_refetch:
|
||||
return self._last_image
|
||||
|
||||
kwargs = {'timeout': 10, 'auth': self._auth}
|
||||
# aiohttp don't support DigestAuth yet
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
def fetch():
|
||||
"""Read image from a URL."""
|
||||
try:
|
||||
response = requests.get(url, timeout=10, auth=self._auth)
|
||||
return response.content
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return self._last_image
|
||||
|
||||
try:
|
||||
response = requests.get(url, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return None
|
||||
self._last_image = yield from self.hass.loop.run_in_executor(
|
||||
None, fetch)
|
||||
# async
|
||||
else:
|
||||
response = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from websession.get(
|
||||
url, auth=self._auth)
|
||||
self._last_image = yield from response.read()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error('Timeout getting camera image')
|
||||
return self._last_image
|
||||
except (aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError) as err:
|
||||
_LOGGER.error('Error getting new camera image: %s', err)
|
||||
return self._last_image
|
||||
finally:
|
||||
if response is not None:
|
||||
self.hass.async_add_job(response.release())
|
||||
|
||||
self._last_url = url
|
||||
self._last_image = response.content
|
||||
return self._last_image
|
||||
|
||||
@property
|
||||
|
||||
@@ -4,9 +4,14 @@ Support for IP Cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.mjpeg/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import closing
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
import voluptuous as vol
|
||||
@@ -15,6 +20,7 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -34,10 +40,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a MJPEG IP Camera."""
|
||||
add_devices([MjpegCamera(config)])
|
||||
yield from async_add_devices([MjpegCamera(hass, config)])
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
@@ -52,11 +59,10 @@ def extract_image_from_mjpeg(stream):
|
||||
return jpg
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class MjpegCamera(Camera):
|
||||
"""An implementation of an IP camera that is reachable over a URL."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
def __init__(self, hass, device_info):
|
||||
"""Initialize a MJPEG camera."""
|
||||
super().__init__()
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
@@ -65,32 +71,64 @@ class MjpegCamera(Camera):
|
||||
self._password = device_info.get(CONF_PASSWORD)
|
||||
self._mjpeg_url = device_info[CONF_MJPEG_URL]
|
||||
|
||||
def camera_stream(self):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
self._auth = None
|
||||
if self._username and self._password:
|
||||
if self._authentication == HTTP_BASIC_AUTHENTICATION:
|
||||
self._auth = aiohttp.BasicAuth(
|
||||
self._username, password=self._password
|
||||
)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
if self._username and self._password:
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
auth = HTTPDigestAuth(self._username, self._password)
|
||||
else:
|
||||
auth = HTTPBasicAuth(self._username, self._password)
|
||||
return requests.get(self._mjpeg_url,
|
||||
auth=auth,
|
||||
stream=True, timeout=10)
|
||||
req = requests.get(
|
||||
self._mjpeg_url, auth=auth, stream=True, timeout=10)
|
||||
else:
|
||||
return requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||
req = requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
with closing(self.camera_stream()) as response:
|
||||
return extract_image_from_mjpeg(response.iter_content(1024))
|
||||
with closing(req) as response:
|
||||
return extract_image_from_mjpeg(response.iter_content(102400))
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
stream = self.camera_stream()
|
||||
return response(
|
||||
stream.iter_content(chunk_size=1024),
|
||||
mimetype=stream.headers[CONTENT_TYPE_HEADER],
|
||||
direct_passthrough=True
|
||||
)
|
||||
# aiohttp don't support DigestAuth -> Fallback
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
yield from super().handle_async_mjpeg_stream(request)
|
||||
return
|
||||
|
||||
# connect to stream
|
||||
websession = async_get_clientsession(self.hass)
|
||||
stream = None
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
stream = yield from websession.get(self._mjpeg_url,
|
||||
auth=self._auth)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
while True:
|
||||
data = yield from stream.content.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
finally:
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
109
homeassistant/components/camera/nest.py
Normal file
109
homeassistant/components/camera/nest.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Support for Nest Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.nest/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
import homeassistant.components.nest as nest
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
|
||||
NEST_BRAND = 'Nest'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a Nest Cam."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
camera_devices = hass.data[nest.DATA_NEST].camera_devices()
|
||||
cameras = [NestCamera(structure, device)
|
||||
for structure, device in camera_devices]
|
||||
add_devices(cameras, True)
|
||||
|
||||
|
||||
class NestCamera(Camera):
|
||||
"""Representation of a Nest Camera."""
|
||||
|
||||
def __init__(self, structure, device):
|
||||
"""Initialize a Nest Camera."""
|
||||
super(NestCamera, self).__init__()
|
||||
self.structure = structure
|
||||
self.device = device
|
||||
self._location = None
|
||||
self._name = None
|
||||
self._is_online = None
|
||||
self._is_streaming = None
|
||||
self._is_video_history_enabled = False
|
||||
# Default to non-NestAware subscribed, but will be fixed during update
|
||||
self._time_between_snapshots = timedelta(seconds=30)
|
||||
self._last_image = None
|
||||
self._next_snapshot_at = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Nest camera should poll periodically."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return self._is_streaming
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the brand of the camera."""
|
||||
return NEST_BRAND
|
||||
|
||||
# This doesn't seem to be getting called regularly, for some reason
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._is_online = self.device.is_online
|
||||
self._is_streaming = self.device.is_streaming
|
||||
self._is_video_history_enabled = self.device.is_video_history_enabled
|
||||
|
||||
if self._is_video_history_enabled:
|
||||
# NestAware allowed 10/min
|
||||
self._time_between_snapshots = timedelta(seconds=6)
|
||||
else:
|
||||
# Otherwise, 2/min
|
||||
self._time_between_snapshots = timedelta(seconds=30)
|
||||
|
||||
def _ready_for_snapshot(self, now):
|
||||
return (self._next_snapshot_at is None or
|
||||
now > self._next_snapshot_at)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
now = utcnow()
|
||||
if self._ready_for_snapshot(now):
|
||||
url = self.device.snapshot_url
|
||||
|
||||
try:
|
||||
response = requests.get(url)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error("Error getting camera image: %s", error)
|
||||
return None
|
||||
|
||||
self._next_snapshot_at = now + self._time_between_snapshots
|
||||
self._last_image = response.content
|
||||
|
||||
return self._last_image
|
||||
@@ -5,12 +5,11 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.netatmo import WelcomeData
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -22,8 +21,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_HOME = 'home'
|
||||
CONF_CAMERAS = 'cameras'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
@@ -39,15 +36,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
for camera_name in data.get_camera_names():
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
add_devices([WelcomeCamera(data, camera_name, home)])
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if config[CONF_CAMERAS] != []:
|
||||
if camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
add_devices([WelcomeCamera(data, camera_name, home)])
|
||||
|
||||
|
||||
class WelcomeCamera(Camera):
|
||||
"""Representation of the images published from Welcome camera."""
|
||||
@@ -61,6 +58,10 @@ class WelcomeCamera(Camera):
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
||||
home=home)['id']
|
||||
self._unique_id = "Welcome_camera {0} - {1}".format(self._name,
|
||||
camera_id)
|
||||
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls(
|
||||
camera=camera_name
|
||||
)
|
||||
@@ -87,31 +88,7 @@ class WelcomeCamera(Camera):
|
||||
"""Return the name of this Netatmo Welcome device."""
|
||||
return self._name
|
||||
|
||||
|
||||
class WelcomeData(object):
|
||||
"""Get the latest data from NetAtmo."""
|
||||
|
||||
def __init__(self, auth, home=None):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.welcomedata = None
|
||||
self.camera_names = []
|
||||
self.home = home
|
||||
|
||||
def get_camera_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.update()
|
||||
if not self.home:
|
||||
for home in self.welcomedata.cameras:
|
||||
for camera in self.welcomedata.cameras[home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
else:
|
||||
for camera in self.welcomedata.cameras[self.home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
return self.camera_names
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.welcomedata = lnetatmo.WelcomeData(self.auth)
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@@ -12,7 +12,8 @@ import shutil
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH)
|
||||
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,10 +36,10 @@ DEFAULT_TIMELAPSE = 1000
|
||||
DEFAULT_VERTICAL_FLIP = 0
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FILE_PATH): cv.isfile,
|
||||
vol.Optional(CONF_FILE_PATH): cv.string,
|
||||
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
||||
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP):
|
||||
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT):
|
||||
vol.Coerce(int),
|
||||
vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
|
||||
@@ -53,6 +54,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def kill_raspistill(*args):
|
||||
"""Kill any previously running raspistill process.."""
|
||||
subprocess.Popen(['killall', 'raspistill'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Raspberry Camera."""
|
||||
if shutil.which("raspistill") is None:
|
||||
@@ -75,11 +83,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
}
|
||||
)
|
||||
|
||||
if not os.access(setup_config[CONF_FILE_PATH], os.W_OK):
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill)
|
||||
|
||||
try:
|
||||
# Try to create an empty file (or open existing) to ensure we have
|
||||
# proper permissions.
|
||||
open(setup_config[CONF_FILE_PATH], 'a').close()
|
||||
|
||||
add_devices([RaspberryCamera(setup_config)])
|
||||
except PermissionError:
|
||||
_LOGGER.error("File path is not writable")
|
||||
return False
|
||||
|
||||
add_devices([RaspberryCamera(setup_config)])
|
||||
except FileNotFoundError:
|
||||
_LOGGER.error("Could not create output file (missing directory?)")
|
||||
return False
|
||||
|
||||
|
||||
class RaspberryCamera(Camera):
|
||||
@@ -93,9 +110,7 @@ class RaspberryCamera(Camera):
|
||||
self._config = device_info
|
||||
|
||||
# Kill if there's raspistill instance
|
||||
subprocess.Popen(['killall', 'raspistill'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT)
|
||||
kill_raspistill()
|
||||
|
||||
cmd_args = [
|
||||
'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH],
|
||||
|
||||
288
homeassistant/components/camera/synology.py
Normal file
288
homeassistant/components/camera/synology.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
Support for Synology Surveillance Station Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.synology/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_create_clientsession)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
|
||||
QUERY_CGI = 'query.cgi'
|
||||
QUERY_API = 'SYNO.API.Info'
|
||||
AUTH_API = 'SYNO.API.Auth'
|
||||
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
|
||||
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
|
||||
SESSION_ID = '0'
|
||||
|
||||
WEBAPI_PATH = '/webapi/'
|
||||
AUTH_PATH = 'auth.cgi'
|
||||
CAMERA_PATH = 'camera.cgi'
|
||||
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
SYNO_API_URL = '{0}{1}{2}'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||
|
||||
# Determine API to use for authentication
|
||||
syno_api_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
||||
|
||||
query_payload = {
|
||||
'api': QUERY_API,
|
||||
'method': 'Query',
|
||||
'version': '1',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
query_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
)
|
||||
|
||||
query_resp = yield from query_req.json()
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_api_url)
|
||||
return False
|
||||
|
||||
finally:
|
||||
if query_req is not None:
|
||||
yield from query_req.release()
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||
|
||||
session_id = yield from get_session_id(
|
||||
hass,
|
||||
websession_init,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url
|
||||
)
|
||||
|
||||
# init websession
|
||||
websession = async_create_clientsession(
|
||||
hass, verify_ssl, cookies={'id': session_id})
|
||||
|
||||
# Use SessionID to get cameras in system
|
||||
syno_camera_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
||||
|
||||
camera_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'List',
|
||||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
camera_req = yield from websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_camera_url)
|
||||
return False
|
||||
|
||||
camera_resp = yield from camera_req.json()
|
||||
cameras = camera_resp['data']['cameras']
|
||||
yield from camera_req.release()
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
for camera in cameras:
|
||||
if not config.get(CONF_WHITELIST):
|
||||
camera_id = camera['id']
|
||||
snapshot_path = camera['snapshot_path']
|
||||
|
||||
device = SynologyCamera(
|
||||
hass,
|
||||
websession,
|
||||
config,
|
||||
camera_id,
|
||||
camera['name'],
|
||||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_path
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, websession, username, password, login_url):
|
||||
"""Get a session id."""
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': username,
|
||||
'passwd': password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
auth_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
)
|
||||
auth_resp = yield from auth_req.json()
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", login_url)
|
||||
return False
|
||||
|
||||
finally:
|
||||
if auth_req is not None:
|
||||
yield from auth_req.release()
|
||||
|
||||
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
||||
def __init__(self, hass, websession, config, camera_id,
|
||||
camera_name, snapshot_path, streaming_path, camera_path,
|
||||
auth_path):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._websession = websession
|
||||
self._name = camera_name
|
||||
self._synology_url = config.get(CONF_URL)
|
||||
self._camera_name = config.get(CONF_CAMERA_NAME)
|
||||
self._stream_id = config.get(CONF_STREAM_ID)
|
||||
self._camera_id = camera_id
|
||||
self._snapshot_path = snapshot_path
|
||||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
image_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._camera_path)
|
||||
|
||||
image_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'GetSnapshot',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
response = yield from self._websession.get(
|
||||
image_url,
|
||||
params=image_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", image_url)
|
||||
return None
|
||||
|
||||
image = yield from response.read()
|
||||
yield from response.release()
|
||||
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
streaming_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
||||
|
||||
streaming_payload = {
|
||||
'api': STREAMING_API,
|
||||
'method': 'Stream',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'
|
||||
}
|
||||
stream = None
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
stream = yield from self._websession.get(
|
||||
streaming_url,
|
||||
params=streaming_payload
|
||||
)
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
while True:
|
||||
data = yield from stream.content.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", streaming_url)
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
finally:
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._name
|
||||
103
homeassistant/components/camera/verisure.py
Normal file
103
homeassistant/components/camera/verisure.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Camera that loads a picture from a local file.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.verisure/
|
||||
"""
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.components.verisure import CONF_SMARTCAM
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Camera."""
|
||||
if not int(hub.config.get(CONF_SMARTCAM, 1)):
|
||||
return False
|
||||
directory_path = hass.config.config_dir
|
||||
if not os.access(directory_path, os.R_OK):
|
||||
_LOGGER.error("file path %s is not readable", directory_path)
|
||||
return False
|
||||
hub.update_smartcam()
|
||||
smartcams = []
|
||||
smartcams.extend([
|
||||
VerisureSmartcam(hass, value.deviceLabel, directory_path)
|
||||
for value in hub.smartcam_status.values()])
|
||||
add_devices(smartcams)
|
||||
|
||||
|
||||
class VerisureSmartcam(Camera):
|
||||
"""Local camera."""
|
||||
|
||||
def __init__(self, hass, device_id, directory_path):
|
||||
"""Initialize Verisure File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._device_id = device_id
|
||||
self._directory_path = directory_path
|
||||
self._image = None
|
||||
self._image_id = None
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
self.delete_image)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
self.check_imagelist()
|
||||
if not self._image:
|
||||
_LOGGER.debug('No image to display')
|
||||
return
|
||||
_LOGGER.debug('Trying to open %s', self._image)
|
||||
with open(self._image, 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
def check_imagelist(self):
|
||||
"""Check the contents of the image list."""
|
||||
hub.update_smartcam_imagelist()
|
||||
if (self._device_id not in hub.smartcam_dict or
|
||||
not hub.smartcam_dict[self._device_id]):
|
||||
return
|
||||
images = hub.smartcam_dict[self._device_id]
|
||||
new_image_id = images[0]
|
||||
_LOGGER.debug('self._device_id=%s, self._images=%s, '
|
||||
'self._new_image_id=%s', self._device_id,
|
||||
images, new_image_id)
|
||||
if (new_image_id == '-1' or
|
||||
self._image_id == new_image_id):
|
||||
_LOGGER.debug('The image is the same, or loading image_id')
|
||||
return
|
||||
_LOGGER.debug('Download new image %s', new_image_id)
|
||||
hub.my_pages.smartcam.download_image(self._device_id,
|
||||
new_image_id,
|
||||
self._directory_path)
|
||||
_LOGGER.debug('Old image_id=%s', self._image_id)
|
||||
self.delete_image(self)
|
||||
|
||||
self._image_id = new_image_id
|
||||
self._image = os.path.join(self._directory_path,
|
||||
'{}{}'.format(
|
||||
self._image_id,
|
||||
'.jpg'))
|
||||
|
||||
def delete_image(self, event):
|
||||
"""Delete an old image."""
|
||||
remove_image = os.path.join(self._directory_path,
|
||||
'{}{}'.format(
|
||||
self._image_id,
|
||||
'.jpg'))
|
||||
try:
|
||||
os.remove(remove_image)
|
||||
_LOGGER.debug('Deleting old image %s', remove_image)
|
||||
except OSError as error:
|
||||
if error.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return hub.smartcam_status[self._device_id].location
|
||||
@@ -58,6 +58,11 @@ ATTR_OPERATION_LIST = "operation_list"
|
||||
ATTR_SWING_MODE = "swing_mode"
|
||||
ATTR_SWING_LIST = "swing_list"
|
||||
|
||||
# The degree of precision for each platform
|
||||
PRECISION_WHOLE = 1
|
||||
PRECISION_HALVES = 0.5
|
||||
PRECISION_TENTHS = 0.1
|
||||
|
||||
CONVERTIBLE_ATTRIBUTE = [
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
@@ -123,7 +128,6 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def set_temperature(hass, temperature=None, entity_id=None,
|
||||
target_temp_high=None, target_temp_low=None,
|
||||
operation_mode=None):
|
||||
@@ -181,7 +185,6 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
"""Setup climate devices."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
@@ -253,7 +256,7 @@ def setup(hass, config):
|
||||
kwargs[value] = convert_temperature(
|
||||
temp,
|
||||
hass.config.units.temperature_unit,
|
||||
climate.unit_of_measurement
|
||||
climate.temperature_unit
|
||||
)
|
||||
else:
|
||||
kwargs[value] = temp
|
||||
@@ -364,11 +367,22 @@ def setup(hass, config):
|
||||
class ClimateDevice(Entity):
|
||||
"""Representation of a climate device."""
|
||||
|
||||
# pylint: disable=too-many-public-methods,no-self-use
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
return self.current_operation or STATE_UNKNOWN
|
||||
if self.current_operation:
|
||||
return self.current_operation
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
if self.unit_of_measurement == TEMP_CELSIUS:
|
||||
return PRECISION_TENTHS
|
||||
else:
|
||||
return PRECISION_WHOLE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@@ -398,17 +412,20 @@ class ClimateDevice(Entity):
|
||||
fan_mode = self.current_fan_mode
|
||||
if fan_mode is not None:
|
||||
data[ATTR_FAN_MODE] = fan_mode
|
||||
data[ATTR_FAN_LIST] = self.fan_list
|
||||
if self.fan_list:
|
||||
data[ATTR_FAN_LIST] = self.fan_list
|
||||
|
||||
operation_mode = self.current_operation
|
||||
if operation_mode is not None:
|
||||
data[ATTR_OPERATION_MODE] = operation_mode
|
||||
data[ATTR_OPERATION_LIST] = self.operation_list
|
||||
if self.operation_list:
|
||||
data[ATTR_OPERATION_LIST] = self.operation_list
|
||||
|
||||
swing_mode = self.current_swing_mode
|
||||
if swing_mode is not None:
|
||||
data[ATTR_SWING_MODE] = swing_mode
|
||||
data[ATTR_SWING_LIST] = self.swing_list
|
||||
if self.swing_list:
|
||||
data[ATTR_SWING_LIST] = self.swing_list
|
||||
|
||||
is_away = self.is_away_mode_on
|
||||
if is_away is not None:
|
||||
@@ -422,7 +439,12 @@ class ClimateDevice(Entity):
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
"""The unit of measurement to display."""
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""The unit of measurement used by the platform."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@@ -534,12 +556,12 @@ class ClimateDevice(Entity):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return convert_temperature(7, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return convert_temperature(35, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit)
|
||||
|
||||
@property
|
||||
def min_humidity(self):
|
||||
@@ -553,16 +575,18 @@ class ClimateDevice(Entity):
|
||||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
if (temp is None or not isinstance(temp, Number) or
|
||||
self.temperature_unit == self.unit_of_measurement):
|
||||
return temp
|
||||
|
||||
value = convert_temperature(temp, self.unit_of_measurement,
|
||||
self.hass.config.units.temperature_unit)
|
||||
value = convert_temperature(temp, self.temperature_unit,
|
||||
self.unit_of_measurement)
|
||||
|
||||
if self.hass.config.units.temperature_unit is TEMP_CELSIUS:
|
||||
decimal_count = 1
|
||||
# Round in the units appropriate
|
||||
if self.precision == PRECISION_HALVES:
|
||||
return round(value * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(value, 1)
|
||||
else:
|
||||
# Users of fahrenheit generally expect integer units.
|
||||
decimal_count = 0
|
||||
|
||||
return round(value, decimal_count)
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(value)
|
||||
|
||||
@@ -21,11 +21,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-public-methods
|
||||
class DemoClimate(ClimateDevice):
|
||||
"""Representation of a demo climate device."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, name, target_temperature, unit_of_measurement,
|
||||
away, current_temperature, current_fan_mode,
|
||||
target_humidity, current_humidity, current_swing_mode,
|
||||
@@ -59,7 +57,7 @@ class DemoClimate(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.climate import (
|
||||
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
class Thermostat(ClimateDevice):
|
||||
"""A thermostat class for Ecobee."""
|
||||
|
||||
@@ -105,12 +104,9 @@ class Thermostat(ClimateDevice):
|
||||
return self.thermostat['name']
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self.thermostat['settings']['useCelsius']:
|
||||
return TEMP_CELSIUS
|
||||
else:
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@@ -199,8 +195,9 @@ class Thermostat(ClimateDevice):
|
||||
mode = self.mode
|
||||
events = self.thermostat['events']
|
||||
for event in events:
|
||||
if event['running']:
|
||||
mode = event['holdClimateRef']
|
||||
if event['holdClimateRef'] == 'away' or \
|
||||
event['type'] == 'autoAway':
|
||||
mode = "away"
|
||||
break
|
||||
return 'away' in mode
|
||||
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
"""
|
||||
Support for eq3 Bluetooth Smart thermostats.
|
||||
Support for eQ-3 Bluetooth Smart thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.eq3btsmart/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
|
||||
from homeassistant.util.temperature import convert
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['bluepy_devices==0.2.0']
|
||||
|
||||
CONF_MAC = 'mac'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MODE = 'mode'
|
||||
ATTR_MODE_READABLE = 'mode_readable'
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_MAC): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICES):
|
||||
vol.Schema({cv.string: DEVICE_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the eq3 BLE thermostats."""
|
||||
"""Setup the eQ-3 BLE thermostats."""
|
||||
devices = []
|
||||
|
||||
for name, device_cfg in config[CONF_DEVICES].items():
|
||||
@@ -28,16 +42,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
|
||||
# pylint: disable=import-error
|
||||
class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Representation of a EQ3 Bluetooth Smart thermostat."""
|
||||
"""Representation of a eQ-3 Bluetooth Smart thermostat."""
|
||||
|
||||
def __init__(self, _mac, _name):
|
||||
"""Initialize the thermostat."""
|
||||
from bluepy_devices.devices import eq3btsmart
|
||||
|
||||
self._name = _name
|
||||
|
||||
self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac)
|
||||
|
||||
@property
|
||||
@@ -46,7 +59,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@@ -70,8 +83,10 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {"mode": self._thermostat.mode,
|
||||
"mode_readable": self._thermostat.mode_readable}
|
||||
return {
|
||||
ATTR_MODE: self._thermostat.mode,
|
||||
ATTR_MODE_READABLE: self._thermostat.mode_readable,
|
||||
}
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
||||
@@ -21,10 +21,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['switch', 'sensor']
|
||||
|
||||
TOL_TEMP = 0.3
|
||||
DEFAULT_TOLERANCE = 0.3
|
||||
DEFAULT_NAME = 'Generic Thermostat'
|
||||
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'Generic Thermostat'
|
||||
CONF_HEATER = 'heater'
|
||||
CONF_SENSOR = 'target_sensor'
|
||||
CONF_MIN_TEMP = 'min_temp'
|
||||
@@ -32,6 +32,7 @@ CONF_MAX_TEMP = 'max_temp'
|
||||
CONF_TARGET_TEMP = 'target_temp'
|
||||
CONF_AC_MODE = 'ac_mode'
|
||||
CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_TOLERANCE = 'tolerance'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -42,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
})
|
||||
|
||||
@@ -56,25 +58,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
target_temp = config.get(CONF_TARGET_TEMP)
|
||||
ac_mode = config.get(CONF_AC_MODE)
|
||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
tolerance = config.get(CONF_TOLERANCE)
|
||||
|
||||
add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration)])
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
class GenericThermostat(ClimateDevice):
|
||||
"""Representation of a GenericThermostat device."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
tolerance):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.heater_entity_id = heater_entity_id
|
||||
self.ac_mode = ac_mode
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@@ -100,7 +103,7 @@ class GenericThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
@@ -147,7 +150,7 @@ class GenericThermostat(ClimateDevice):
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
@@ -160,7 +163,7 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
self._update_temp(new_state)
|
||||
self._control_heating()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
@@ -195,24 +198,30 @@ class GenericThermostat(ClimateDevice):
|
||||
return
|
||||
|
||||
if self.ac_mode:
|
||||
too_hot = self._cur_temp - self._target_temp > TOL_TEMP
|
||||
is_cooling = self._is_device_active
|
||||
if too_hot and not is_cooling:
|
||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
elif not too_hot and is_cooling:
|
||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
if is_cooling:
|
||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||
if too_hot:
|
||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_cold = self._target_temp - self._cur_temp > TOL_TEMP
|
||||
is_heating = self._is_device_active
|
||||
|
||||
if too_cold and not is_heating:
|
||||
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
elif not too_cold and is_heating:
|
||||
_LOGGER.info('Turning off heater %s', self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
if is_heating:
|
||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||
if too_hot:
|
||||
_LOGGER.info('Turning off heater %s',
|
||||
self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
|
||||
@property
|
||||
def _is_device_active(self):
|
||||
|
||||
@@ -1,56 +1,54 @@
|
||||
"""
|
||||
Support for the PRT Heatmiser themostats using the V3 protocol.
|
||||
|
||||
See https://github.com/andylockran/heatmiserV3 for more info on the
|
||||
heatmiserV3 module dependency.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.heatmiser/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
import voluptuous as vol
|
||||
|
||||
CONF_IPADDRESS = 'ipaddress'
|
||||
CONF_PORT = 'port'
|
||||
CONF_TSTATS = 'tstats'
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ["heatmiserV3==0.9.1"]
|
||||
REQUIREMENTS = ['heatmiserV3==0.9.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IPADDRESS = 'ipaddress'
|
||||
CONF_TSTATS = 'tstats'
|
||||
|
||||
TSTATS_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_IPADDRESS): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Required(CONF_TSTATS, default={}):
|
||||
vol.Schema({cv.string: TSTATS_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the heatmiser thermostat."""
|
||||
from heatmiserV3 import heatmiser, connection
|
||||
|
||||
ipaddress = str(config[CONF_IPADDRESS])
|
||||
port = str(config[CONF_PORT])
|
||||
|
||||
if ipaddress is None or port is None:
|
||||
_LOGGER.error("Missing required configuration items %s or %s",
|
||||
CONF_IPADDRESS, CONF_PORT)
|
||||
return False
|
||||
ipaddress = config.get(CONF_IPADDRESS)
|
||||
port = str(config.get(CONF_PORT))
|
||||
tstats = config.get(CONF_TSTATS)
|
||||
|
||||
serport = connection.connection(ipaddress, port)
|
||||
serport.open()
|
||||
|
||||
tstats = []
|
||||
if CONF_TSTATS in config:
|
||||
tstats = config[CONF_TSTATS]
|
||||
|
||||
if tstats is None:
|
||||
_LOGGER.error("No thermostats configured.")
|
||||
return False
|
||||
|
||||
for tstat in tstats:
|
||||
for thermostat, tstat in tstats.items():
|
||||
add_devices([
|
||||
HeatmiserV3Thermostat(
|
||||
heatmiser,
|
||||
tstat.get("id"),
|
||||
tstat.get("name"),
|
||||
serport)
|
||||
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
|
||||
])
|
||||
return
|
||||
|
||||
@@ -58,7 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class HeatmiserV3Thermostat(ClimateDevice):
|
||||
"""Representation of a HeatmiserV3 thermostat."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
def __init__(self, heatmiser, device, name, serport):
|
||||
"""Initialize the thermostat."""
|
||||
self.heatmiser = heatmiser
|
||||
@@ -69,7 +66,7 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
self._id = device
|
||||
self.dcb = None
|
||||
self.update()
|
||||
self._target_temperature = int(self.dcb.get("roomset"))
|
||||
self._target_temperature = int(self.dcb.get('roomset'))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -77,7 +74,7 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@@ -85,9 +82,9 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if self.dcb is not None:
|
||||
low = self.dcb.get("floortemplow ")
|
||||
high = self.dcb.get("floortemphigh")
|
||||
temp = (high*256 + low)/10.0
|
||||
low = self.dcb.get('floortemplow ')
|
||||
high = self.dcb.get('floortemphigh')
|
||||
temp = (high * 256 + low) / 10.0
|
||||
self._current_temperature = temp
|
||||
else:
|
||||
self._current_temperature = None
|
||||
|
||||
@@ -5,10 +5,11 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.homematic/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.util.temperature import convert
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
@@ -29,19 +30,20 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMThermostat,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||
class HMThermostat(HMDevice, ClimateDevice):
|
||||
"""Representation of a Homematic thermostat."""
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@@ -95,13 +97,9 @@ class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if not self.available:
|
||||
if not self.available or temperature is None:
|
||||
return None
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return self._hmdevice.actionNodeData('MANU_MODE', temperature)
|
||||
self._hmdevice.set_temperature(temperature)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
|
||||
@@ -100,7 +100,6 @@ def _setup_us(username, password, config, add_devices):
|
||||
class RoundThermostat(ClimateDevice):
|
||||
"""Representation of a Honeywell Round Connected thermostat."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
def __init__(self, device, zone_id, master, away_temp):
|
||||
"""Initialize the thermostat."""
|
||||
self.device = device
|
||||
@@ -120,7 +119,7 @@ class RoundThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@@ -197,7 +196,6 @@ class RoundThermostat(ClimateDevice):
|
||||
self._is_dhw = False
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Representation of a Honeywell US Thermostat."""
|
||||
|
||||
@@ -217,7 +215,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return (TEMP_CELSIUS if self._device.temperature_unit == 'C'
|
||||
else TEMP_FAHRENHEIT)
|
||||
@@ -225,7 +223,6 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
self._device.refresh()
|
||||
return self._device.current_temperature
|
||||
|
||||
@property
|
||||
@@ -276,3 +273,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Set the system mode (Cool, Heat, etc)."""
|
||||
if hasattr(self._device, ATTR_SYSTEM_MODE):
|
||||
self._device.system_mode = operation_mode
|
||||
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
self._device.refresh()
|
||||
|
||||
@@ -56,6 +56,8 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
|
||||
self._away = False # not yet supported
|
||||
self._is_fan_on = False # not yet supported
|
||||
self._current_temp = None
|
||||
self._target_temp = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -63,23 +65,19 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
return True
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
return knx2_to_float(self.value('temperature'))
|
||||
return self._current_temp
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
return knx2_to_float(self.value('setpoint'))
|
||||
return self._target_temp
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -94,3 +92,12 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def update(self):
|
||||
"""Update KNX climate."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
super().update()
|
||||
|
||||
self._current_temp = knx2_to_float(self.value('temperature'))
|
||||
self._target_temp = knx2_to_float(self.value('setpoint'))
|
||||
|
||||
@@ -24,7 +24,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors climate."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
if not gateways:
|
||||
return
|
||||
|
||||
for gateway in gateways:
|
||||
if float(gateway.protocol_version) < 1.5:
|
||||
continue
|
||||
pres = gateway.const.Presentation
|
||||
@@ -37,7 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
map_sv_types, devices, add_devices, MySensorsHVAC))
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-public-methods
|
||||
class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
"""Representation of a MySensorsHVAC hvac."""
|
||||
|
||||
@@ -47,7 +51,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
return self.gateway.optimistic
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return (TEMP_CELSIUS
|
||||
if self.gateway.metric else TEMP_FAHRENHEIT)
|
||||
|
||||
@@ -5,14 +5,17 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.nest/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.components.nest as nest
|
||||
|
||||
from homeassistant.components.nest import DATA_NEST
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -22,15 +25,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
})
|
||||
|
||||
STATE_ECO = 'eco'
|
||||
STATE_HEAT_COOL = 'heat-cool'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Nest thermostat."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Setting up nest thermostat")
|
||||
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
add_devices([NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in nest.devices()])
|
||||
|
||||
add_devices(
|
||||
[NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in hass.data[DATA_NEST].devices()],
|
||||
True
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method,too-many-public-methods
|
||||
class NestThermostat(ClimateDevice):
|
||||
"""Representation of a Nest thermostat."""
|
||||
|
||||
@@ -40,97 +54,109 @@ class NestThermostat(ClimateDevice):
|
||||
self.structure = structure
|
||||
self.device = device
|
||||
self._fan_list = [STATE_ON, STATE_AUTO]
|
||||
self._operation_list = [STATE_HEAT, STATE_COOL, STATE_AUTO,
|
||||
STATE_OFF]
|
||||
|
||||
# Not all nest devices support cooling and heating remove unused
|
||||
self._operation_list = [STATE_OFF]
|
||||
|
||||
# Add supported nest thermostat features
|
||||
if self.device.can_heat:
|
||||
self._operation_list.append(STATE_HEAT)
|
||||
|
||||
if self.device.can_cool:
|
||||
self._operation_list.append(STATE_COOL)
|
||||
|
||||
if self.device.can_heat and self.device.can_cool:
|
||||
self._operation_list.append(STATE_AUTO)
|
||||
|
||||
self._operation_list.append(STATE_ECO)
|
||||
|
||||
# feature of device
|
||||
self._has_fan = self.device.has_fan
|
||||
|
||||
# data attributes
|
||||
self._away = None
|
||||
self._location = None
|
||||
self._name = None
|
||||
self._humidity = None
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._temperature_scale = None
|
||||
self._mode = None
|
||||
self._fan = None
|
||||
self._eco_temperature = None
|
||||
self._is_locked = None
|
||||
self._locked_temperature = None
|
||||
self._min_temperature = None
|
||||
self._max_temperature = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
location = self.device.where
|
||||
name = self.device.name
|
||||
if location is None:
|
||||
return name
|
||||
else:
|
||||
if name == '':
|
||||
return location.capitalize()
|
||||
else:
|
||||
return location.capitalize() + '(' + name + ')'
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
# Move these to Thermostat Device and make them global
|
||||
return {
|
||||
"humidity": self.device.humidity,
|
||||
"target_humidity": self.device.target_humidity,
|
||||
}
|
||||
return self._temperature_scale
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.device.temperature
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self.device.mode == 'cool':
|
||||
return STATE_COOL
|
||||
elif self.device.mode == 'heat':
|
||||
return STATE_HEAT
|
||||
elif self.device.mode == 'range':
|
||||
if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
||||
return self._mode
|
||||
elif self._mode == STATE_HEAT_COOL:
|
||||
return STATE_AUTO
|
||||
elif self.device.mode == 'off':
|
||||
return STATE_OFF
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.device.mode != 'range' and not self.is_away_mode_on:
|
||||
return self.device.target
|
||||
if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on:
|
||||
return self._target_temperature
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.is_away_mode_on and self.device.away_temperature[0]:
|
||||
# away_temperature is always a low, high tuple
|
||||
return self.device.away_temperature[0]
|
||||
if self.device.mode == 'range':
|
||||
return self.device.target[0]
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[0]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.is_away_mode_on and self.device.away_temperature[1]:
|
||||
# away_temperature is always a low, high tuple
|
||||
return self.device.away_temperature[1]
|
||||
if self.device.mode == 'range':
|
||||
return self.device.target[1]
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[1]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if away mode is on."""
|
||||
return self.structure.away
|
||||
return self._away
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
|
||||
if self.device.mode == 'range':
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -139,14 +165,11 @@ class NestThermostat(ClimateDevice):
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.device.mode = 'heat'
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.device.mode = 'cool'
|
||||
if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
||||
device_mode = operation_mode
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.device.mode = 'range'
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.device.mode = 'off'
|
||||
device_mode = STATE_HEAT_COOL
|
||||
self.device.mode = device_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@@ -164,7 +187,12 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
return STATE_ON if self.device.fan else STATE_AUTO
|
||||
if self._has_fan:
|
||||
# Return whether the fan is on
|
||||
return STATE_ON if self._fan else STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
@@ -178,21 +206,29 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Identify min_temp in Nest API or defaults if not available."""
|
||||
temp = self.device.away_temperature.low
|
||||
if temp is None:
|
||||
return super().min_temp
|
||||
else:
|
||||
return temp
|
||||
return self._min_temperature
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Identify max_temp in Nest API or defaults if not available."""
|
||||
temp = self.device.away_temperature.high
|
||||
if temp is None:
|
||||
return super().max_temp
|
||||
else:
|
||||
return temp
|
||||
return self._max_temperature
|
||||
|
||||
def update(self):
|
||||
"""Python-nest has its own mechanism for staying up to date."""
|
||||
pass
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._humidity = self.device.humidity,
|
||||
self._temperature = self.device.temperature
|
||||
self._mode = self.device.mode
|
||||
self._target_temperature = self.device.target
|
||||
self._fan = self.device.fan
|
||||
self._away = self.structure.away == 'away'
|
||||
self._eco_temperature = self.device.eco_temperature
|
||||
self._locked_temperature = self.device.locked_temperature
|
||||
self._min_temperature = self.device.min_temperature
|
||||
self._max_temperature = self.device.max_temperature
|
||||
self._is_locked = self.device.is_locked
|
||||
if self.device.temperature_scale == 'C':
|
||||
self._temperature_scale = TEMP_CELSIUS
|
||||
else:
|
||||
self._temperature_scale = TEMP_FAHRENHEIT
|
||||
|
||||
177
homeassistant/components/climate/netatmo.py
Executable file
177
homeassistant/components/climate/netatmo.py
Executable file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Support for Netatmo Smart Thermostat.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['netatmo']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RELAY = 'relay'
|
||||
CONF_THERMOSTAT = 'thermostat'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE = 14
|
||||
# # The default offeset is 2 hours (when you use the thermostat itself)
|
||||
DEFAULT_TIME_OFFSET = 7200
|
||||
# # Return cached results if last scan was less then this time ago
|
||||
# # NetAtmo Data is uploaded to server every hour
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_RELAY): cv.string,
|
||||
vol.Optional(CONF_THERMOSTAT, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the NetAtmo Thermostat."""
|
||||
netatmo = get_component('netatmo')
|
||||
device = config.get(CONF_RELAY)
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
data = ThermostatData(netatmo.NETATMO_AUTH, device)
|
||||
for module_name in data.get_module_names():
|
||||
if CONF_THERMOSTAT in config:
|
||||
if config[CONF_THERMOSTAT] != [] and \
|
||||
module_name not in config[CONF_THERMOSTAT]:
|
||||
continue
|
||||
add_callback_devices([NetatmoThermostat(data, module_name)])
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
|
||||
class NetatmoThermostat(ClimateDevice):
|
||||
"""Representation a Netatmo thermostat."""
|
||||
|
||||
def __init__(self, data, module_name, away_temp=None):
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self._state = None
|
||||
self._name = module_name
|
||||
self._target_temperature = None
|
||||
self._away = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._data.current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the current state of the thermostat."""
|
||||
state = self._data.thermostatdata.relay_cmd
|
||||
if state == 0:
|
||||
return STATE_IDLE
|
||||
elif state == 100:
|
||||
return STATE_HEAT
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
mode = "away"
|
||||
temp = None
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
mode = "program"
|
||||
temp = None
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
|
||||
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
|
||||
"""Set new target temperature for 2 hours."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
mode = "manual"
|
||||
self._data.thermostatdata.setthermpoint(
|
||||
mode, temperature, endTimeOffset)
|
||||
self._target_temperature = temperature
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from NetAtmo API and updates the states."""
|
||||
self._data.update()
|
||||
self._target_temperature = self._data.thermostatdata.setpoint_temp
|
||||
self._away = self._data.setpoint_mode == 'away'
|
||||
|
||||
|
||||
class ThermostatData(object):
|
||||
"""Get the latest data from Netatmo."""
|
||||
|
||||
def __init__(self, auth, device=None):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.thermostatdata = None
|
||||
self.module_names = []
|
||||
self.device = device
|
||||
self.current_temperature = None
|
||||
self.target_temperature = None
|
||||
self.setpoint_mode = None
|
||||
# self.operation =
|
||||
|
||||
def get_module_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.update()
|
||||
if not self.device:
|
||||
for device in self.thermostatdata.modules:
|
||||
for module in self.thermostatdata.modules[device].values():
|
||||
self.module_names.append(module['module_name'])
|
||||
else:
|
||||
for module in self.thermostatdata.modules[self.device].values():
|
||||
self.module_names.append(module['module_name'])
|
||||
return self.module_names
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.thermostatdata = lnetatmo.ThermostatData(self.auth)
|
||||
self.target_temperature = self.thermostatdata.setpoint_temp
|
||||
self.setpoint_mode = self.thermostatdata.setpoint_mode
|
||||
self.current_temperature = self.thermostatdata.temp
|
||||
@@ -7,12 +7,13 @@ https://home-assistant.io/components/climate.proliphix/
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE,
|
||||
ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['proliphix==0.3.1']
|
||||
REQUIREMENTS = ['proliphix==0.4.1']
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
|
||||
@@ -36,7 +37,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([ProliphixThermostat(pdp)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class ProliphixThermostat(ClimateDevice):
|
||||
"""Representation a Proliphix thermostat."""
|
||||
|
||||
@@ -61,6 +61,15 @@ class ProliphixThermostat(ClimateDevice):
|
||||
"""Return the name of the thermostat."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system.
|
||||
|
||||
Proliphix temperature values are passed back and forth in the
|
||||
API as tenths of degrees F (i.e. 690 for 69 degrees).
|
||||
"""
|
||||
return PRECISION_TENTHS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
@@ -69,7 +78,7 @@ class ProliphixThermostat(ClimateDevice):
|
||||
}
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ https://home-assistant.io/components/climate.radiotherm/
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
from urllib.error import URLError
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -24,10 +23,19 @@ ATTR_FAN = 'fan'
|
||||
ATTR_MODE = 'mode'
|
||||
|
||||
CONF_HOLD_TEMP = 'hold_temp'
|
||||
CONF_AWAY_TEMPERATURE_HEAT = 'away_temperature_heat'
|
||||
CONF_AWAY_TEMPERATURE_COOL = 'away_temperature_cool'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE_HEAT = 60
|
||||
DEFAULT_AWAY_TEMPERATURE_COOL = 85
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE_HEAT,
|
||||
default=DEFAULT_AWAY_TEMPERATURE_HEAT): vol.Coerce(float),
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE_COOL,
|
||||
default=DEFAULT_AWAY_TEMPERATURE_COOL): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
@@ -46,24 +54,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return False
|
||||
|
||||
hold_temp = config.get(CONF_HOLD_TEMP)
|
||||
away_temps = [
|
||||
config.get(CONF_AWAY_TEMPERATURE_HEAT),
|
||||
config.get(CONF_AWAY_TEMPERATURE_COOL)
|
||||
]
|
||||
tstats = []
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
tstat = radiotherm.get_thermostat(host)
|
||||
tstats.append(RadioThermostat(tstat, hold_temp))
|
||||
except (URLError, OSError):
|
||||
tstats.append(RadioThermostat(tstat, hold_temp, away_temps))
|
||||
except OSError:
|
||||
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
|
||||
host)
|
||||
|
||||
add_devices(tstats)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class RadioThermostat(ClimateDevice):
|
||||
"""Representation of a Radio Thermostat."""
|
||||
|
||||
def __init__(self, device, hold_temp):
|
||||
def __init__(self, device, hold_temp, away_temps):
|
||||
"""Initialize the thermostat."""
|
||||
self.device = device
|
||||
self.set_time()
|
||||
@@ -71,7 +82,12 @@ class RadioThermostat(ClimateDevice):
|
||||
self._current_temperature = None
|
||||
self._current_operation = STATE_IDLE
|
||||
self._name = None
|
||||
self.hold_temp = hold_temp
|
||||
self._fmode = None
|
||||
self._tmode = None
|
||||
self._hold_temp = hold_temp
|
||||
self._away = False
|
||||
self._away_temps = away_temps
|
||||
self._prev_temp = None
|
||||
self.update()
|
||||
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
|
||||
|
||||
@@ -81,7 +97,7 @@ class RadioThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@@ -89,8 +105,8 @@ class RadioThermostat(ClimateDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
ATTR_FAN: self.device.fmode['human'],
|
||||
ATTR_MODE: self.device.tmode['human']
|
||||
ATTR_FAN: self._fmode,
|
||||
ATTR_MODE: self._tmode,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -113,14 +129,22 @@ class RadioThermostat(ClimateDevice):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
self._current_temperature = self.device.temp['raw']
|
||||
self._name = self.device.name['raw']
|
||||
if self.device.tmode['human'] == 'Cool':
|
||||
self._fmode = self.device.fmode['human']
|
||||
self._tmode = self.device.tmode['human']
|
||||
|
||||
if self._tmode == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
self._current_operation = STATE_COOL
|
||||
elif self.device.tmode['human'] == 'Heat':
|
||||
elif self._tmode == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
self._current_operation = STATE_HEAT
|
||||
else:
|
||||
@@ -132,10 +156,10 @@ class RadioThermostat(ClimateDevice):
|
||||
if temperature is None:
|
||||
return
|
||||
if self._current_operation == STATE_COOL:
|
||||
self.device.t_cool = temperature
|
||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||
elif self._current_operation == STATE_HEAT:
|
||||
self.device.t_heat = temperature
|
||||
if self.hold_temp:
|
||||
self.device.t_heat = round(temperature * 2.0) / 2.0
|
||||
if self._hold_temp or self._away:
|
||||
self.device.hold = 1
|
||||
else:
|
||||
self.device.hold = 0
|
||||
@@ -156,6 +180,26 @@ class RadioThermostat(ClimateDevice):
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.device.tmode = 3
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.device.t_cool = self._target_temperature
|
||||
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0
|
||||
elif operation_mode == STATE_HEAT:
|
||||
self.device.t_heat = self._target_temperature
|
||||
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on.
|
||||
|
||||
The RTCOA app simulates away mode by using a hold.
|
||||
"""
|
||||
away_temp = None
|
||||
if not self._away:
|
||||
self._prev_temp = self._target_temperature
|
||||
if self._current_operation == STATE_HEAT:
|
||||
away_temp = self._away_temps[0]
|
||||
elif self._current_operation == STATE_COOL:
|
||||
away_temp = self._away_temps[1]
|
||||
self._away = True
|
||||
self.set_temperature(temperature=away_temp)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self._away = False
|
||||
self.set_temperature(temperature=self._prev_temp)
|
||||
|
||||
@@ -8,7 +8,10 @@ import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE)
|
||||
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
@@ -28,7 +31,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
device in VERA_DEVICES['climate'])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
"""Representation of a Vera Thermostat."""
|
||||
|
||||
@@ -93,9 +95,15 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
self._state = self.vera_device.get_hvac_mode()
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
vera_temp_units = (
|
||||
self.vera_device.vera_controller.temperature_units)
|
||||
|
||||
if vera_temp_units == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
331
homeassistant/components/climate/wink.py
Normal file
331
homeassistant/components/climate/wink.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Support for Wink thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.wink/
|
||||
"""
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_CURRENT_HUMIDITY)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, STATE_ON,
|
||||
STATE_OFF, STATE_UNKNOWN)
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
STATE_AUX = 'aux'
|
||||
STATE_ECO = 'eco'
|
||||
|
||||
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
ATTR_SMART_TEMPERATURE = "smart_temperature"
|
||||
ATTR_ECO_TARGET = "eco_target"
|
||||
ATTR_OCCUPIED = "occupied"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink thermostat."""
|
||||
import pywink
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
add_devices(WinkThermostat(thermostat, hass, temp_unit)
|
||||
for thermostat in pywink.get_thermostats())
|
||||
|
||||
|
||||
# pylint: disable=abstract-method,too-many-public-methods, too-many-branches
|
||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink thermostat."""
|
||||
|
||||
def __init__(self, wink, hass, temp_unit):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink, hass)
|
||||
wink = get_component('wink')
|
||||
self._config_temp_unit = temp_unit
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
# The Wink API always returns temp in Celsius
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {}
|
||||
target_temp_high = self.target_temperature_high
|
||||
target_temp_low = self.target_temperature_low
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
if target_temp_low is not None:
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
|
||||
if self.external_temperature:
|
||||
data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display(
|
||||
self.external_temperature)
|
||||
|
||||
if self.smart_temperature:
|
||||
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
|
||||
|
||||
if self.occupied:
|
||||
data[ATTR_OCCUPIED] = self.occupied
|
||||
|
||||
if self.eco_target:
|
||||
data[ATTR_ECO_TARGET] = self.eco_target
|
||||
|
||||
current_humidity = self.current_humidity
|
||||
if current_humidity is not None:
|
||||
data[ATTR_CURRENT_HUMIDITY] = current_humidity
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.wink.current_temperature()
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
if self.wink.current_humidity() is not None:
|
||||
# The API states humidity will be a float 0-1
|
||||
# the only example API response with humidity listed show an int
|
||||
# This will address both possibilities
|
||||
if self.wink.current_humidity() < 1:
|
||||
return self.wink.current_humidity() * 100
|
||||
else:
|
||||
return self.wink.current_humidity()
|
||||
|
||||
@property
|
||||
def external_temperature(self):
|
||||
"""Return the current external temperature."""
|
||||
return self.wink.current_external_temperature()
|
||||
|
||||
@property
|
||||
def smart_temperature(self):
|
||||
"""Return the current average temp of all remote sensor."""
|
||||
return self.wink.current_smart_temperature()
|
||||
|
||||
@property
|
||||
def eco_target(self):
|
||||
"""Return status of eco target (Is the termostat in eco mode)."""
|
||||
return self.wink.eco_target()
|
||||
|
||||
@property
|
||||
def occupied(self):
|
||||
"""Return status of if the thermostat has detected occupancy."""
|
||||
return self.wink.occupied()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
elif self.wink.current_hvac_mode() == 'cool_only':
|
||||
current_op = STATE_COOL
|
||||
elif self.wink.current_hvac_mode() == 'heat_only':
|
||||
current_op = STATE_HEAT
|
||||
elif self.wink.current_hvac_mode() == 'aux':
|
||||
current_op = STATE_HEAT
|
||||
elif self.wink.current_hvac_mode() == 'auto':
|
||||
current_op = STATE_AUTO
|
||||
elif self.wink.current_hvac_mode() == 'eco':
|
||||
current_op = STATE_ECO
|
||||
else:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
|
||||
@property
|
||||
def target_humidity(self):
|
||||
"""Return the humidity we try to reach."""
|
||||
target_hum = None
|
||||
if self.wink.current_humidifier_mode() == 'on':
|
||||
if self.wink.current_humidifier_set_point() is not None:
|
||||
target_hum = self.wink.current_humidifier_set_point() * 100
|
||||
elif self.wink.current_dehumidifier_mode() == 'on':
|
||||
if self.wink.current_dehumidifier_set_point() is not None:
|
||||
target_hum = self.wink.current_dehumidifier_set_point() * 100
|
||||
else:
|
||||
target_hum = None
|
||||
return target_hum
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.current_operation != STATE_AUTO and not self.is_away_mode_on:
|
||||
if self.current_operation == STATE_COOL:
|
||||
return self.wink.current_max_set_point()
|
||||
elif self.current_operation == STATE_HEAT:
|
||||
return self.wink.current_min_set_point()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return self.wink.current_min_set_point()
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the higher bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return self.wink.current_max_set_point()
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if away mode is on."""
|
||||
return self.wink.away()
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if aux heater."""
|
||||
if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on():
|
||||
return True
|
||||
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp is not None:
|
||||
if self.current_operation == STATE_COOL:
|
||||
target_temp_high = target_temp
|
||||
if self.current_operation == STATE_HEAT:
|
||||
target_temp_low = target_temp
|
||||
if target_temp_low is not None:
|
||||
target_temp_low = target_temp_low
|
||||
if target_temp_high is not None:
|
||||
target_temp_high = target_temp_high
|
||||
self.wink.set_temperature(target_temp_low, target_temp_high)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.wink.set_operation_mode('heat_only')
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.wink.set_operation_mode('cool_only')
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.wink.set_operation_mode('auto')
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.wink.set_operation_mode('off')
|
||||
elif operation_mode == STATE_AUX:
|
||||
self.wink.set_operation_mode('aux')
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.wink.set_operation_mode('eco')
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
op_list = ['off']
|
||||
modes = self.wink.hvac_modes()
|
||||
if 'cool_only' in modes:
|
||||
op_list.append(STATE_COOL)
|
||||
if 'heat_only' in modes or 'aux' in modes:
|
||||
op_list.append(STATE_HEAT)
|
||||
if 'auto' in modes:
|
||||
op_list.append(STATE_AUTO)
|
||||
if 'eco' in modes:
|
||||
op_list.append(STATE_ECO)
|
||||
return op_list
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.wink.set_away_mode()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.wink.set_away_mode(False)
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
if self.wink.current_fan_mode() == 'on':
|
||||
return STATE_ON
|
||||
elif self.wink.current_fan_mode() == 'auto':
|
||||
return STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
if self.wink.has_fan():
|
||||
return self.wink.fan_modes()
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Turn fan on/off."""
|
||||
self.wink.set_fan_mode(fan.lower())
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
self.set_operation_mode(STATE_AUX)
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
self.set_operation_mode(STATE_AUTO)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
minimum = 7 # Default minimum
|
||||
min_min = self.wink.min_min_set_point()
|
||||
min_max = self.wink.min_max_set_point()
|
||||
return_value = minimum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if min_min:
|
||||
return_value = min_min
|
||||
else:
|
||||
return_value = minimum
|
||||
elif self.current_operation == STATE_COOL:
|
||||
if min_max:
|
||||
return_value = min_max
|
||||
else:
|
||||
return_value = minimum
|
||||
elif self.current_operation == STATE_AUTO:
|
||||
if min_min and min_max:
|
||||
return_value = min(min_min, min_max)
|
||||
else:
|
||||
return_value = minimum
|
||||
else:
|
||||
return_value = minimum
|
||||
return return_value
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
maximum = 35 # Default maximum
|
||||
max_min = self.wink.max_min_set_point()
|
||||
max_max = self.wink.max_max_set_point()
|
||||
return_value = maximum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if max_min:
|
||||
return_value = max_min
|
||||
else:
|
||||
return_value = maximum
|
||||
elif self.current_operation == STATE_COOL:
|
||||
if max_max:
|
||||
return_value = max_max
|
||||
else:
|
||||
return_value = maximum
|
||||
elif self.current_operation == STATE_AUTO:
|
||||
if max_min and max_max:
|
||||
return_value = min(max_min, max_max)
|
||||
else:
|
||||
return_value = maximum
|
||||
else:
|
||||
return_value = maximum
|
||||
return return_value
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for ZWave climate devices.
|
||||
Support for Z-Wave climate devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.zwave/
|
||||
@@ -8,8 +8,7 @@ https://home-assistant.io/components/climate.zwave/
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ATTR_OPERATION_MODE)
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (
|
||||
@@ -18,44 +17,23 @@ from homeassistant.const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'ZWave Climate'
|
||||
DEFAULT_NAME = 'Z-Wave Climate'
|
||||
|
||||
REMOTEC = 0x5254
|
||||
REMOTEC_ZXT_120 = 0x8377
|
||||
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
|
||||
|
||||
HORSTMANN = 0x0059
|
||||
HORSTMANN_HRT4_ZW = 0x3
|
||||
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
|
||||
ATTR_OPERATING_STATE = 'operating_state'
|
||||
ATTR_FAN_STATE = 'fan_state'
|
||||
|
||||
WORKAROUND_ZXT_120 = 'zxt_120'
|
||||
WORKAROUND_HRT4_ZW = 'hrt4_zw'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120,
|
||||
HORSTMANN_HRT4_ZW_THERMOSTAT: WORKAROUND_HRT4_ZW
|
||||
}
|
||||
|
||||
SET_TEMP_TO_INDEX = {
|
||||
'Heat': 1,
|
||||
'Cool': 2,
|
||||
'Auto': 3,
|
||||
'Aux Heat': 4,
|
||||
'Resume': 5,
|
||||
'Fan Only': 6,
|
||||
'Furnace': 7,
|
||||
'Dry Air': 8,
|
||||
'Moist Air': 9,
|
||||
'Auto Changeover': 10,
|
||||
'Heat Econ': 11,
|
||||
'Cool Econ': 12,
|
||||
'Away': 13,
|
||||
'Unknown': 14
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the ZWave Climate devices."""
|
||||
"""Set up the Z-Wave Climate devices."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
@@ -69,30 +47,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Represents a ZWave Climate device."""
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, value, temp_unit):
|
||||
"""Initialize the zwave climate device."""
|
||||
"""Initialize the Z-Wave climate device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._index = value.index
|
||||
self._node = value.node
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_operation = None
|
||||
self._operation_list = None
|
||||
self._operating_state = None
|
||||
self._current_fan_mode = None
|
||||
self._fan_list = None
|
||||
self._fan_state = None
|
||||
self._current_swing_mode = None
|
||||
self._swing_list = None
|
||||
self._unit = temp_unit
|
||||
self._index_operation = None
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
self._hrt4_zw = None
|
||||
self.update_properties()
|
||||
# register listener
|
||||
dispatcher.connect(
|
||||
@@ -107,17 +84,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||
" workaround")
|
||||
self._zxt_120 = 1
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_HRT4_ZW:
|
||||
_LOGGER.debug("Horstmann HRT4-ZW Zwave Thermostat"
|
||||
" workaround")
|
||||
self._hrt4_zw = 1
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
def update_properties(self):
|
||||
@@ -126,23 +99,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
self._current_operation = value.data
|
||||
self._index_operation = SET_TEMP_TO_INDEX.get(
|
||||
self._current_operation)
|
||||
self._operation_list = list(value.data_items)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s",
|
||||
self._current_operation)
|
||||
# Current Temp
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
if value.label == 'Temperature':
|
||||
self._current_temperature = int(value.data)
|
||||
self._current_temperature = round((float(value.data)), 1)
|
||||
self._unit = value.units
|
||||
# Fan Mode
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
self._current_fan_mode = value.data
|
||||
self._fan_list = list(value.data_items)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
@@ -150,9 +123,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
@@ -162,30 +136,39 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = int(self._current_temperature)
|
||||
break
|
||||
if self.current_operation is not None and \
|
||||
self.current_operation != 'Off':
|
||||
if self._index_operation != value.index:
|
||||
continue
|
||||
if self._zxt_120:
|
||||
temps = []
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
temps.append((round(float(value.data)), 1))
|
||||
if value.index == self._index:
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
break
|
||||
self._target_temperature = int(value.data)
|
||||
break
|
||||
_LOGGER.debug("Device can't set setpoint based on operation mode."
|
||||
" Defaulting to index=1")
|
||||
self._target_temperature = int(value.data)
|
||||
else:
|
||||
self._target_temperature = round((float(value.data)), 1)
|
||||
# Operating state
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const
|
||||
.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).values()):
|
||||
self._operating_state = value.data
|
||||
|
||||
# Fan operating state
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE)
|
||||
.values()):
|
||||
self._fan_state = value.data
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling on ZWave."""
|
||||
"""No polling on Z-Wave."""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -209,7 +192,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
return self._swing_list
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self._unit == 'C':
|
||||
return TEMP_CELSIUS
|
||||
@@ -244,53 +227,19 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
else:
|
||||
return
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
_LOGGER.debug("set_temperature operation_mode=%s", operation_mode)
|
||||
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if operation_mode is not None:
|
||||
setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode)
|
||||
if value.index != setpoint_mode:
|
||||
continue
|
||||
_LOGGER.debug("setpoint_mode=%s", setpoint_mode)
|
||||
value.data = temperature
|
||||
break
|
||||
|
||||
if self.current_operation is not None:
|
||||
if self._hrt4_zw and self.current_operation == 'Off':
|
||||
# HRT4-ZW can change setpoint when off.
|
||||
value.data = int(temperature)
|
||||
if self._index_operation != value.index:
|
||||
continue
|
||||
_LOGGER.debug("self._index_operation=%s and"
|
||||
" self._current_operation=%s",
|
||||
self._index_operation,
|
||||
self._current_operation)
|
||||
if value.index == self._index:
|
||||
if self._zxt_120:
|
||||
_LOGGER.debug("zxt_120: Setting new setpoint for %s, "
|
||||
" operation=%s, temp=%s",
|
||||
self._index_operation,
|
||||
self._current_operation, temperature)
|
||||
# ZXT-120 does not support get setpoint
|
||||
self._target_temperature = temperature
|
||||
# ZXT-120 responds only to whole int
|
||||
value.data = round(temperature, 0)
|
||||
self._target_temperature = temperature
|
||||
self.update_ha_state()
|
||||
break
|
||||
else:
|
||||
_LOGGER.debug("Setting new setpoint for %s, "
|
||||
"operation=%s, temp=%s",
|
||||
self._index_operation,
|
||||
self._current_operation, temperature)
|
||||
value.data = temperature
|
||||
break
|
||||
else:
|
||||
_LOGGER.debug("Setting new setpoint for no known "
|
||||
"operation mode. Index=1 and "
|
||||
"temperature=%s", temperature)
|
||||
value.data = temperature
|
||||
self.update_ha_state()
|
||||
break
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
@@ -323,3 +272,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
value.index == 33:
|
||||
value.data = bytes(swing_mode, 'utf-8')
|
||||
break
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
data = super().device_state_attributes
|
||||
if self._operating_state:
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state,
|
||||
if self._fan_state:
|
||||
data[ATTR_FAN_STATE] = self._fan_state
|
||||
return data
|
||||
|
||||
@@ -8,7 +8,8 @@ the user has submitted configuration information.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
_INSTANCES = {}
|
||||
@@ -33,10 +34,10 @@ STATE_CONFIGURE = 'configure'
|
||||
STATE_CONFIGURED = 'configured'
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def request_config(
|
||||
hass, name, callback, description=None, description_image=None,
|
||||
submit_caption=None, fields=None, link_name=None, link_url=None):
|
||||
submit_caption=None, fields=None, link_name=None, link_url=None,
|
||||
entity_picture=None):
|
||||
"""Create a new request for configuration.
|
||||
|
||||
Will return an ID to be used for sequent calls.
|
||||
@@ -46,7 +47,7 @@ def request_config(
|
||||
request_id = instance.request_config(
|
||||
name, callback,
|
||||
description, description_image, submit_caption,
|
||||
fields, link_name, link_url)
|
||||
fields, link_name, link_url, entity_picture)
|
||||
|
||||
_REQUESTS[request_id] = instance
|
||||
|
||||
@@ -100,11 +101,10 @@ class Configurator(object):
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def request_config(
|
||||
self, name, callback,
|
||||
description, description_image, submit_caption,
|
||||
fields, link_name, link_url):
|
||||
fields, link_name, link_url, entity_picture):
|
||||
"""Setup a request for configuration."""
|
||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
|
||||
|
||||
@@ -119,6 +119,7 @@ class Configurator(object):
|
||||
ATTR_CONFIGURE_ID: request_id,
|
||||
ATTR_FIELDS: fields,
|
||||
ATTR_FRIENDLY_NAME: name,
|
||||
ATTR_ENTITY_PICTURE: entity_picture,
|
||||
}
|
||||
|
||||
data.update({
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.12.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.14.0']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
|
||||
@@ -135,12 +135,19 @@ def setup(hass, config):
|
||||
params = service.data.copy()
|
||||
params.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
if method:
|
||||
for cover in component.extract_from_service(service):
|
||||
getattr(cover, method['method'])(**params)
|
||||
if not method:
|
||||
return
|
||||
|
||||
if cover.should_poll:
|
||||
cover.update_ha_state(True)
|
||||
covers = component.extract_from_service(service)
|
||||
|
||||
for cover in covers:
|
||||
getattr(cover, method['method'])(**params)
|
||||
|
||||
for cover in covers:
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
cover.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@@ -60,11 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class CommandCover(CoverDevice):
|
||||
"""Representation a command line cover."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, command_open, command_close, command_stop,
|
||||
command_state, value_template):
|
||||
"""Initialize the cover."""
|
||||
|
||||
@@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class DemoCover(CoverDevice):
|
||||
"""Representation of a demo cover."""
|
||||
|
||||
# pylint: disable=no-self-use, too-many-instance-attributes
|
||||
# pylint: disable=no-self-use
|
||||
def __init__(self, hass, name, position=None, tilt_position=None):
|
||||
"""Initialize the cover."""
|
||||
self.hass = hass
|
||||
|
||||
275
homeassistant/components/cover/garadget.py
Normal file
275
homeassistant/components/cover/garadget.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Platform for the garadget cover component.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/garadget/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD,\
|
||||
CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN,\
|
||||
CONF_COVERS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEFAULT_NAME = 'Garadget'
|
||||
|
||||
ATTR_SIGNAL_STRENGTH = "wifi signal strength (dB)"
|
||||
ATTR_TIME_IN_STATE = "time in state"
|
||||
ATTR_SENSOR_STRENGTH = "sensor reflection rate"
|
||||
ATTR_AVAILABLE = "available"
|
||||
|
||||
STATE_OPENING = "opening"
|
||||
STATE_CLOSING = "closing"
|
||||
STATE_STOPPED = "stopped"
|
||||
STATE_OFFLINE = "offline"
|
||||
|
||||
STATES_MAP = {
|
||||
"open": STATE_OPEN,
|
||||
"opening": STATE_OPENING,
|
||||
"closed": STATE_CLOSED,
|
||||
"closing": STATE_CLOSING,
|
||||
"stopped": STATE_STOPPED
|
||||
}
|
||||
|
||||
|
||||
# Validation of the user's configuration
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_DEVICE): cv.string,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_ACCESS_TOKEN): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo covers."""
|
||||
covers = []
|
||||
devices = config.get(CONF_COVERS, {})
|
||||
|
||||
_LOGGER.debug(devices)
|
||||
|
||||
for device_id, device_config in devices.items():
|
||||
args = {
|
||||
"name": device_config.get(CONF_NAME),
|
||||
"device_id": device_config.get(CONF_DEVICE, device_id),
|
||||
"username": device_config.get(CONF_USERNAME),
|
||||
"password": device_config.get(CONF_PASSWORD),
|
||||
"access_token": device_config.get(CONF_ACCESS_TOKEN)
|
||||
}
|
||||
|
||||
covers.append(GaradgetCover(hass, args))
|
||||
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
class GaradgetCover(CoverDevice):
|
||||
"""Representation of a demo cover."""
|
||||
|
||||
# pylint: disable=no-self-use, too-many-instance-attributes
|
||||
def __init__(self, hass, args):
|
||||
"""Initialize the cover."""
|
||||
self.particle_url = 'https://api.particle.io'
|
||||
self.hass = hass
|
||||
self._name = args['name']
|
||||
self.device_id = args['device_id']
|
||||
self.access_token = args['access_token']
|
||||
self.obtained_token = False
|
||||
self._username = args['username']
|
||||
self._password = args['password']
|
||||
self._state = STATE_UNKNOWN
|
||||
self.time_in_state = None
|
||||
self.signal = None
|
||||
self.sensor = None
|
||||
self._unsub_listener_cover = None
|
||||
self._available = True
|
||||
|
||||
if self.access_token is None:
|
||||
self.access_token = self.get_token()
|
||||
self._obtained_token = True
|
||||
|
||||
# Lets try to get the configured name if not provided.
|
||||
try:
|
||||
if self._name is None:
|
||||
doorconfig = self._get_variable("doorConfig")
|
||||
if doorconfig["nme"] is not None:
|
||||
self._name = doorconfig["nme"]
|
||||
self.update()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to server: %(reason)s',
|
||||
dict(reason=ex))
|
||||
self._state = STATE_OFFLINE
|
||||
self._available = False
|
||||
self._name = DEFAULT_NAME
|
||||
except KeyError as ex:
|
||||
_LOGGER.warning('Garadget device %(device)s seems to be offline',
|
||||
dict(device=self.device_id))
|
||||
self._name = DEFAULT_NAME
|
||||
self._state = STATE_OFFLINE
|
||||
self._available = False
|
||||
|
||||
def __del__(self):
|
||||
"""Try to remove token."""
|
||||
if self._obtained_token is True:
|
||||
if self.access_token is not None:
|
||||
self.remove_token()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for a demo cover."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
data = {}
|
||||
|
||||
if self.signal is not None:
|
||||
data[ATTR_SIGNAL_STRENGTH] = self.signal
|
||||
|
||||
if self.time_in_state is not None:
|
||||
data[ATTR_TIME_IN_STATE] = self.time_in_state
|
||||
|
||||
if self.sensor is not None:
|
||||
data[ATTR_SENSOR_STRENGTH] = self.sensor
|
||||
|
||||
if self.access_token is not None:
|
||||
data[CONF_ACCESS_TOKEN] = self.access_token
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
def get_token(self):
|
||||
"""Get new token for usage during this session."""
|
||||
args = {
|
||||
'grant_type': 'password',
|
||||
'username': self._username,
|
||||
'password': self._password
|
||||
}
|
||||
url = '{}/oauth/token'.format(self.particle_url)
|
||||
ret = requests.post(url,
|
||||
auth=('particle', 'particle'),
|
||||
data=args)
|
||||
|
||||
return ret.json()['access_token']
|
||||
|
||||
def remove_token(self):
|
||||
"""Remove authorization token from API."""
|
||||
ret = requests.delete('{}/v1/access_tokens/{}'.format(
|
||||
self.particle_url,
|
||||
self.access_token),
|
||||
auth=(self._username, self._password))
|
||||
return ret.text
|
||||
|
||||
def _start_watcher(self, command):
|
||||
"""Start watcher."""
|
||||
_LOGGER.debug("Starting Watcher for command: %s ", command)
|
||||
if self._unsub_listener_cover is None:
|
||||
self._unsub_listener_cover = track_utc_time_change(
|
||||
self.hass, self._check_state)
|
||||
|
||||
def _check_state(self, now):
|
||||
"""Check the state of the service during an operation."""
|
||||
self.update()
|
||||
self.update_ha_state()
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the cover."""
|
||||
if self._state not in ["close", "closing"]:
|
||||
ret = self._put_command("setState", "close")
|
||||
self._start_watcher('close')
|
||||
return ret.get('return_value') == 1
|
||||
|
||||
def open_cover(self):
|
||||
"""Open the cover."""
|
||||
if self._state not in ["open", "opening"]:
|
||||
ret = self._put_command("setState", "open")
|
||||
self._start_watcher('open')
|
||||
return ret.get('return_value') == 1
|
||||
|
||||
def stop_cover(self):
|
||||
"""Stop the door where it is."""
|
||||
if self._state not in ["stopped"]:
|
||||
ret = self._put_command("setState", "stop")
|
||||
self._start_watcher('stop')
|
||||
return ret['return_value'] == 1
|
||||
|
||||
def update(self):
|
||||
"""Get updated status from API."""
|
||||
try:
|
||||
status = self._get_variable("doorStatus")
|
||||
_LOGGER.debug("Current Status: %s", status['status'])
|
||||
self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN)
|
||||
self.time_in_state = status['time']
|
||||
self.signal = status['signal']
|
||||
self.sensor = status['sensor']
|
||||
self._availble = True
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to server: %(reason)s',
|
||||
dict(reason=ex))
|
||||
self._state = STATE_OFFLINE
|
||||
except KeyError as ex:
|
||||
_LOGGER.warning('Garadget device %(device)s seems to be offline',
|
||||
dict(device=self.device_id))
|
||||
self._state = STATE_OFFLINE
|
||||
|
||||
if self._state not in [STATE_CLOSING, STATE_OPENING]:
|
||||
if self._unsub_listener_cover is not None:
|
||||
self._unsub_listener_cover()
|
||||
self._unsub_listener_cover = None
|
||||
|
||||
def _get_variable(self, var):
|
||||
"""Get latest status."""
|
||||
url = '{}/v1/devices/{}/{}?access_token={}'.format(
|
||||
self.particle_url,
|
||||
self.device_id,
|
||||
var,
|
||||
self.access_token,
|
||||
)
|
||||
ret = requests.get(url)
|
||||
result = {}
|
||||
for pairs in ret.json()['result'].split('|'):
|
||||
key = pairs.split('=')
|
||||
result[key[0]] = key[1]
|
||||
return result
|
||||
|
||||
def _put_command(self, func, arg=None):
|
||||
"""Send commands to API."""
|
||||
params = {'access_token': self.access_token}
|
||||
if arg:
|
||||
params['command'] = arg
|
||||
url = '{}/v1/devices/{}/{}'.format(
|
||||
self.particle_url,
|
||||
self.device_id,
|
||||
func)
|
||||
ret = requests.post(url, data=params)
|
||||
return ret.json()
|
||||
@@ -12,7 +12,8 @@ import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.cover import CoverDevice,\
|
||||
ATTR_POSITION
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,15 +25,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMCover,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HMCover(homematic.HMDevice, CoverDevice):
|
||||
class HMCover(HMDevice, CoverDevice):
|
||||
"""Represents a Homematic Cover in Home Assistant."""
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.const import (
|
||||
@@ -67,7 +68,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class MqttCover(CoverDevice):
|
||||
"""Representation of a cover that can be controlled using MQTT."""
|
||||
|
||||
@@ -90,29 +90,30 @@ class MqttCover(CoverDevice):
|
||||
self._retain = retain
|
||||
self._optimistic = optimistic or state_topic is None
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._state_open:
|
||||
self._state = False
|
||||
_LOGGER.warning("state=%s", int(self._state))
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._state_closed:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload.isnumeric() and 0 <= int(payload) <= 100:
|
||||
if int(payload) > 0:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
self._position = int(payload)
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Payload is not True, False, or integer (0-100): %s",
|
||||
payload)
|
||||
|
||||
if self._state_topic is None:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
|
||||
@@ -18,7 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for covers."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
if not gateways:
|
||||
return
|
||||
|
||||
for gateway in gateways:
|
||||
pres = gateway.const.Presentation
|
||||
set_req = gateway.const.SetReq
|
||||
map_sv_types = {
|
||||
|
||||
@@ -40,7 +40,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice):
|
||||
"""Representation of an rfxtrx cover."""
|
||||
|
||||
|
||||
@@ -63,11 +63,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class RPiGPIOCover(CoverDevice):
|
||||
"""Representation of a Raspberry GPIO cover."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, relay_pin, state_pin, state_pull_mode,
|
||||
relay_time):
|
||||
"""Initialize the cover."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user