mirror of
https://github.com/home-assistant/core.git
synced 2026-01-07 08:07:16 +01:00
Compare commits
778 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
833508fbbb | ||
|
|
1b71ce32e4 | ||
|
|
c49751542f | ||
|
|
2e3a27e418 | ||
|
|
7566bb5aed | ||
|
|
fc1f6ee0f0 | ||
|
|
cb839eff0f | ||
|
|
2bc87bfcf0 | ||
|
|
44be80145b | ||
|
|
8cb1e17ad8 | ||
|
|
2b53729708 | ||
|
|
a566804f7f | ||
|
|
2a5fac3b9d | ||
|
|
8459b241a2 | ||
|
|
825f94f47f | ||
|
|
8ef2abfca7 | ||
|
|
2372419d42 | ||
|
|
27f3081b74 | ||
|
|
13e72f48a8 | ||
|
|
9fcbe68fac | ||
|
|
0999129f48 | ||
|
|
3180c8b0fb | ||
|
|
37cd63ea5a | ||
|
|
3dc70436f1 | ||
|
|
674682e88f | ||
|
|
ba7fccba34 | ||
|
|
ccba858ae1 | ||
|
|
b0a3d084fb | ||
|
|
45eb611007 | ||
|
|
0eb3e49880 | ||
|
|
c5cb28d41f | ||
|
|
7d43ad6a37 | ||
|
|
b589dbf26c | ||
|
|
23b97b9105 | ||
|
|
f11d4319d2 | ||
|
|
4ba58d0760 | ||
|
|
c076dbe7e4 | ||
|
|
e7aea5c571 | ||
|
|
24ec8c545b | ||
|
|
6c456ade6a | ||
|
|
e9b997de3e | ||
|
|
53506821d4 | ||
|
|
6fa60c464b | ||
|
|
fadff1855f | ||
|
|
652063537b | ||
|
|
bcd8a69dfc | ||
|
|
663aeb11dc | ||
|
|
727ab956cf | ||
|
|
26c76e3399 | ||
|
|
0adb240fd6 | ||
|
|
e836674a30 | ||
|
|
65b8f9764a | ||
|
|
1a9ea11665 | ||
|
|
08f545d67b | ||
|
|
e472436b84 | ||
|
|
783e9a5f8c | ||
|
|
f4b1a8e42d | ||
|
|
3b44f91395 | ||
|
|
cff3bed1f0 | ||
|
|
9fe43714c6 | ||
|
|
569f5c111f | ||
|
|
9487bd455a | ||
|
|
f2d4dd25f0 | ||
|
|
998d8c1771 | ||
|
|
add0afe31a | ||
|
|
534aa0e4b5 | ||
|
|
6e9669c18d | ||
|
|
8fdeebc50d | ||
|
|
d0d61d1b5f | ||
|
|
e8ad36feb6 | ||
|
|
9da239178c | ||
|
|
acdba7a27c | ||
|
|
e0c5b44994 | ||
|
|
595600dea5 | ||
|
|
ad212d8dd4 | ||
|
|
86709427b6 | ||
|
|
36a663adeb | ||
|
|
517fb2e983 | ||
|
|
9677bc081e | ||
|
|
c69f37500a | ||
|
|
cd8935cbd2 | ||
|
|
2f26b0084f | ||
|
|
2bff03836b | ||
|
|
390086bb7e | ||
|
|
c018071218 | ||
|
|
9014e26845 | ||
|
|
1c4da0c4a6 | ||
|
|
bba997e484 | ||
|
|
bf98b793c5 | ||
|
|
1617fbea4c | ||
|
|
4d44c0feff | ||
|
|
5a5dad689b | ||
|
|
c3388d63a1 | ||
|
|
ee6acadae2 | ||
|
|
80a3220b88 | ||
|
|
99ded8a0a6 | ||
|
|
c6c166645d | ||
|
|
0daf38d18c | ||
|
|
5ec30ce1e6 | ||
|
|
fb91b05051 | ||
|
|
c36c2be372 | ||
|
|
598f093bf0 | ||
|
|
b9306a5e52 | ||
|
|
ac2298189e | ||
|
|
60508f7215 | ||
|
|
ddd2003629 | ||
|
|
20ababec3e | ||
|
|
d3b261a25d | ||
|
|
3906250c9e | ||
|
|
22a1b99e57 | ||
|
|
62dc737ea3 | ||
|
|
993866a314 | ||
|
|
51bdd06d1f | ||
|
|
d2804b0a27 | ||
|
|
c863b9614c | ||
|
|
f47572d3c0 | ||
|
|
9bd29589d5 | ||
|
|
f29904f1b5 | ||
|
|
234495ed05 | ||
|
|
09dbd94467 | ||
|
|
bd58a0de7d | ||
|
|
dd7e6edf61 | ||
|
|
b752ca3bef | ||
|
|
9c1bc18def | ||
|
|
2a5751c09d | ||
|
|
8d48164f25 | ||
|
|
b2695e498d | ||
|
|
16a1a4e0b1 | ||
|
|
191e32f6cf | ||
|
|
978a79d369 | ||
|
|
cf88d8a1b9 | ||
|
|
2707d35a86 | ||
|
|
7ea776dff4 | ||
|
|
bd93f10d3c | ||
|
|
c8a464d8f9 | ||
|
|
5ac52b74e0 | ||
|
|
7595401dcb | ||
|
|
ae4e792651 | ||
|
|
2b86059fd0 | ||
|
|
e593117ab6 | ||
|
|
c61611d2b4 | ||
|
|
73de749411 | ||
|
|
cb51553c2d | ||
|
|
8beb9c2b28 | ||
|
|
70649dfe22 | ||
|
|
b01dceaff2 | ||
|
|
ef16c53e46 | ||
|
|
40d7857f3b | ||
|
|
81b1d08d35 | ||
|
|
99f4509c2b | ||
|
|
f915a1c809 | ||
|
|
6cd599b7df | ||
|
|
435b49fb96 | ||
|
|
3084ac1625 | ||
|
|
2bf17cba8e | ||
|
|
ca3cc27e40 | ||
|
|
fbb8a54c39 | ||
|
|
b0fd2342db | ||
|
|
58f3690ef6 | ||
|
|
286476f0d6 | ||
|
|
fdf93d1829 | ||
|
|
262ea14e5a | ||
|
|
c77d013f43 | ||
|
|
48fe2d18e8 | ||
|
|
3394916a68 | ||
|
|
9ae6a3402c | ||
|
|
85487612d5 | ||
|
|
bd51143ac1 | ||
|
|
0a25d30ba6 | ||
|
|
bddfe24753 | ||
|
|
1d7ecc22f9 | ||
|
|
703eea0c93 | ||
|
|
b70b23ef83 | ||
|
|
1a9727c75a | ||
|
|
e6006e9beb | ||
|
|
4008bf5611 | ||
|
|
edcb242b6d | ||
|
|
0081764ddc | ||
|
|
bb5484edac | ||
|
|
63820a78d9 | ||
|
|
61a3b4ffdb | ||
|
|
1b3c3494e8 | ||
|
|
b2d37f5257 | ||
|
|
206e38a2ab | ||
|
|
fe56844a3a | ||
|
|
692b2644c7 | ||
|
|
f263a931f7 | ||
|
|
301077ded9 | ||
|
|
415af5e257 | ||
|
|
9ce02d2717 | ||
|
|
4b2fdd243a | ||
|
|
032d6963d8 | ||
|
|
13bda2669e | ||
|
|
568c6c16fa | ||
|
|
92bd932679 | ||
|
|
bfb49c2e58 | ||
|
|
89f5a938c7 | ||
|
|
9ce4755f8a | ||
|
|
b342c43b09 | ||
|
|
95e98925d1 | ||
|
|
53f08e313f | ||
|
|
9fb73c1bab | ||
|
|
98e4d514a5 | ||
|
|
79eb75f26f | ||
|
|
ff960c0c7a | ||
|
|
52d2139904 | ||
|
|
e687ca781f | ||
|
|
ff72c5e456 | ||
|
|
be43c3bcfe | ||
|
|
4ad0152a44 | ||
|
|
eb763764b3 | ||
|
|
cd96d7b2ec | ||
|
|
dee47d50ec | ||
|
|
c8f2810fac | ||
|
|
ff9f500c51 | ||
|
|
0c0e0c36af | ||
|
|
a8fdd76f44 | ||
|
|
45ef34ff81 | ||
|
|
343d1384a3 | ||
|
|
9f0f7394fb | ||
|
|
5fce2e2b47 | ||
|
|
8fbef5b002 | ||
|
|
3e082b5ce6 | ||
|
|
7c99567b65 | ||
|
|
7b3d17bae4 | ||
|
|
12affa1469 | ||
|
|
477f7ec01e | ||
|
|
7bf8d4ab12 | ||
|
|
3b4faa74a0 | ||
|
|
2518587534 | ||
|
|
273a43be02 | ||
|
|
72fb64695e | ||
|
|
bf44dc422c | ||
|
|
bf58945680 | ||
|
|
0cfc7256ac | ||
|
|
0d62f472cb | ||
|
|
ad5a11ba3d | ||
|
|
4dea55b29c | ||
|
|
0f2cfe7f27 | ||
|
|
9fc8a8f679 | ||
|
|
f40efe0110 | ||
|
|
c361b0c450 | ||
|
|
0f24fea6bb | ||
|
|
0911166c9c | ||
|
|
8fad97a47a | ||
|
|
9cfcd38c1e | ||
|
|
979a8f8772 | ||
|
|
6314aabc6f | ||
|
|
931bceefd9 | ||
|
|
78f3e01854 | ||
|
|
5801d78017 | ||
|
|
184f2be83e | ||
|
|
1ae8b6ee08 | ||
|
|
170763ef2f | ||
|
|
507c658fe9 | ||
|
|
3e5462ebff | ||
|
|
8617177ff1 | ||
|
|
df78eecc1b | ||
|
|
5908b55bba | ||
|
|
e5d76c53fb | ||
|
|
f26aff4885 | ||
|
|
32b0712089 | ||
|
|
867010240a | ||
|
|
0428559f69 | ||
|
|
dfd15900c7 | ||
|
|
e993d095cb | ||
|
|
26fb3d7faa | ||
|
|
b0073b437f | ||
|
|
e04b01daad | ||
|
|
020669fc60 | ||
|
|
d897a07d0b | ||
|
|
a6b63b669e | ||
|
|
ab9b915731 | ||
|
|
0a0b33af03 | ||
|
|
f391cbae27 | ||
|
|
27865f58f1 | ||
|
|
3fdb0002a7 | ||
|
|
298e6eeef1 | ||
|
|
cea2de5eb5 | ||
|
|
3b537f6e2a | ||
|
|
ef7fd9f380 | ||
|
|
b3b7cf3fa7 | ||
|
|
45ff15bc85 | ||
|
|
bdb4d754ae | ||
|
|
00c6df54b2 | ||
|
|
2bebfec3a6 | ||
|
|
264be67787 | ||
|
|
06aded1a4d | ||
|
|
9eda04b787 | ||
|
|
8a0facb747 | ||
|
|
81cf0dacfe | ||
|
|
e1d2d201c8 | ||
|
|
3639a4470c | ||
|
|
94d9aa0c5f | ||
|
|
a06eea444a | ||
|
|
ce3a5972c7 | ||
|
|
f48ce3d437 | ||
|
|
dfe3219f3f | ||
|
|
254256c08f | ||
|
|
f1d37fc849 | ||
|
|
08bcf84170 | ||
|
|
24e0bb198a | ||
|
|
263dbe5d81 | ||
|
|
3e6f4d0e5a | ||
|
|
181e68b027 | ||
|
|
2e3ec121d1 | ||
|
|
a6e455a070 | ||
|
|
a08e5efe53 | ||
|
|
a507ed0af8 | ||
|
|
068b037944 | ||
|
|
38d2702e3c | ||
|
|
93b9ec0b0f | ||
|
|
22cefc7e64 | ||
|
|
60f6109cbf | ||
|
|
24d299e266 | ||
|
|
444805df10 | ||
|
|
a08293cff7 | ||
|
|
0d48a8eec6 | ||
|
|
8a204fd15b | ||
|
|
1887bac37e | ||
|
|
d6af26b589 | ||
|
|
a5ae77ab93 | ||
|
|
8ab5978db3 | ||
|
|
3e3f710b12 | ||
|
|
6d20a84f0e | ||
|
|
b996632965 | ||
|
|
eaf81150ea | ||
|
|
7db37a3834 | ||
|
|
55daea5169 | ||
|
|
594a5b7d29 | ||
|
|
3a765692e7 | ||
|
|
2d2e8034d6 | ||
|
|
f3ccbda054 | ||
|
|
7166d53e2b | ||
|
|
e36f27d6fd | ||
|
|
11930d5f20 | ||
|
|
df35159cb4 | ||
|
|
4d52875229 | ||
|
|
8bd5f66c57 | ||
|
|
872b6cf16b | ||
|
|
8e14e803cb | ||
|
|
101b39300b | ||
|
|
b159484a79 | ||
|
|
725e1ddfc1 | ||
|
|
a17e60208d | ||
|
|
6a625bdb37 | ||
|
|
630734ca15 | ||
|
|
7fd687f59c | ||
|
|
4bd6776443 | ||
|
|
2532d67b9a | ||
|
|
df8596e896 | ||
|
|
2497dd5e33 | ||
|
|
553920780f | ||
|
|
8852e52601 | ||
|
|
23165cbd1a | ||
|
|
23f06b0040 | ||
|
|
5ec6f25d4e | ||
|
|
79c9d3ba10 | ||
|
|
ba7178dc0c | ||
|
|
2c7bc6eaf8 | ||
|
|
c50b00226c | ||
|
|
fb1fafefab | ||
|
|
98620d8ce8 | ||
|
|
d385e9645d | ||
|
|
e9cdbe5d8c | ||
|
|
6e75c5427c | ||
|
|
1676df6a5f | ||
|
|
17cbd0f3c9 | ||
|
|
2d7d8848cb | ||
|
|
3939460814 | ||
|
|
36bc7f8175 | ||
|
|
0396725fe9 | ||
|
|
c3974e540b | ||
|
|
74c249e57d | ||
|
|
da4e630f54 | ||
|
|
ffbafa687a | ||
|
|
7718f70c5f | ||
|
|
0de2681783 | ||
|
|
7e08e8bd51 | ||
|
|
2388d62755 | ||
|
|
fab958d789 | ||
|
|
f8127a3902 | ||
|
|
3426487277 | ||
|
|
cfb0b00c0c | ||
|
|
852eef8046 | ||
|
|
05c9c57500 | ||
|
|
5c4529d044 | ||
|
|
3fa080a795 | ||
|
|
0977be1842 | ||
|
|
4270bc7abb | ||
|
|
a04c6d5830 | ||
|
|
f287955422 | ||
|
|
2bc7e58780 | ||
|
|
947218d51c | ||
|
|
49683181d1 | ||
|
|
89c7c80e42 | ||
|
|
6b059489a6 | ||
|
|
1cbf9792d7 | ||
|
|
437ffc8337 | ||
|
|
9cb3c9034f | ||
|
|
1dcc51cbdf | ||
|
|
022d8fb816 | ||
|
|
1e17b2fd63 | ||
|
|
b45dad507a | ||
|
|
8ed3024026 | ||
|
|
d042b3d7d1 | ||
|
|
4d3743f3f7 | ||
|
|
181eca4b45 | ||
|
|
dbc59ad1a7 | ||
|
|
82f59ba984 | ||
|
|
d35077271d | ||
|
|
aec61b7c86 | ||
|
|
e01a0f91d6 | ||
|
|
8fed405da7 | ||
|
|
3442b6741d | ||
|
|
f5093b474a | ||
|
|
05676ba18b | ||
|
|
66c6f9cdd6 | ||
|
|
5a9013cda5 | ||
|
|
d78e75db66 | ||
|
|
d04ba3f86d | ||
|
|
ed6cd0ccfa | ||
|
|
88d2a6ab80 | ||
|
|
fe7012549e | ||
|
|
f013619e69 | ||
|
|
78144bc6de | ||
|
|
f6ae2d338d | ||
|
|
99f7e2bd97 | ||
|
|
b1079cb493 | ||
|
|
0deef34881 | ||
|
|
2350ce96a6 | ||
|
|
de1ff1e952 | ||
|
|
d13bcf8412 | ||
|
|
456ff4e84b | ||
|
|
89a19c89a7 | ||
|
|
a86bf81768 | ||
|
|
5e675677ad | ||
|
|
ff416c0e7a | ||
|
|
170b8671b9 | ||
|
|
ee6d6a8859 | ||
|
|
1d2fd8a2e9 | ||
|
|
92f13ff60d | ||
|
|
5c434f143e | ||
|
|
646ed5de52 | ||
|
|
27c1806897 | ||
|
|
6909be1cc7 | ||
|
|
223bc187dc | ||
|
|
c971d61422 | ||
|
|
e122692b46 | ||
|
|
76874e1cbc | ||
|
|
d348f09d3d | ||
|
|
64f18c62f4 | ||
|
|
be2e202618 | ||
|
|
07f20676cb | ||
|
|
a30ca4307b | ||
|
|
8d52eba484 | ||
|
|
8e05a5c12b | ||
|
|
25fe6ec536 | ||
|
|
30a1fedce8 | ||
|
|
4e569ac0c3 | ||
|
|
8a6370f7c9 | ||
|
|
874cccd530 | ||
|
|
e1a5e5a8ba | ||
|
|
a9917e7a56 | ||
|
|
ef7ce5eb1b | ||
|
|
7fc9ac0931 | ||
|
|
e2029e3970 | ||
|
|
7e2fc19f5a | ||
|
|
c48c8710b7 | ||
|
|
b6bed1dfab | ||
|
|
948f29544a | ||
|
|
6310deb5c2 | ||
|
|
cfded7eab9 | ||
|
|
0ef4340099 | ||
|
|
24a9da85c0 | ||
|
|
71baa6532e | ||
|
|
4c9e7c2da4 | ||
|
|
5958e6a60f | ||
|
|
3e7a737bff | ||
|
|
dd48fb04a3 | ||
|
|
d5612b5ccc | ||
|
|
8a1687accb | ||
|
|
53351423dd | ||
|
|
75fb8ef98b | ||
|
|
989638b266 | ||
|
|
d028c33e7f | ||
|
|
0a2e949e0a | ||
|
|
1bea8747ac | ||
|
|
e54394e906 | ||
|
|
c384fd9653 | ||
|
|
3560fa754c | ||
|
|
101a6ab07c | ||
|
|
eb1ca20cfc | ||
|
|
ae286a550b | ||
|
|
c5330a13b6 | ||
|
|
6ab4a408d2 | ||
|
|
1202134964 | ||
|
|
bbbb44b999 | ||
|
|
54e0cc1304 | ||
|
|
f9e07e617c | ||
|
|
95a528a75f | ||
|
|
51b0cbefe3 | ||
|
|
8d8b07abd5 | ||
|
|
15d345c4ef | ||
|
|
676c94561b | ||
|
|
02ad9c3574 | ||
|
|
890197e407 | ||
|
|
9ee123f5ce | ||
|
|
14aa4e7694 | ||
|
|
49d51e5040 | ||
|
|
31130f902b | ||
|
|
8603f1a047 | ||
|
|
a34786fb2d | ||
|
|
8e51c12010 | ||
|
|
e3d176f479 | ||
|
|
e37619acc1 | ||
|
|
85fa88c8b3 | ||
|
|
7935c2504e | ||
|
|
7018806802 | ||
|
|
a7f34bbce9 | ||
|
|
e6683b4c84 | ||
|
|
991c457430 | ||
|
|
401e92f84e | ||
|
|
1dc5fa145f | ||
|
|
dff4f6ce48 | ||
|
|
56b3cb0583 | ||
|
|
d0f089975d | ||
|
|
26960283a0 | ||
|
|
f5cc40024d | ||
|
|
d42b5a93dd | ||
|
|
3dfc49d311 | ||
|
|
dc8424032b | ||
|
|
f164a5a65f | ||
|
|
d74a2b68c1 | ||
|
|
458598546d | ||
|
|
3f6d30ed06 | ||
|
|
28ff1f7ac2 | ||
|
|
60aacff827 | ||
|
|
42359b3b48 | ||
|
|
b98d2e2485 | ||
|
|
5281892e69 | ||
|
|
70064e4c69 | ||
|
|
b2d8feb979 | ||
|
|
9a7a0f28b1 | ||
|
|
c7c47a18a9 | ||
|
|
5726159dd4 | ||
|
|
0e00de8a33 | ||
|
|
266b13b3cb | ||
|
|
c1bb7d5cc2 | ||
|
|
ae47da7bce | ||
|
|
f01b5b0040 | ||
|
|
40485a6e89 | ||
|
|
7ea7fc8d38 | ||
|
|
86baed4e52 | ||
|
|
b4b779c49d | ||
|
|
0143752d94 | ||
|
|
e910ecfd5f | ||
|
|
4d74fc2d07 | ||
|
|
f9c1675c95 | ||
|
|
76fb2447a5 | ||
|
|
2fae86bbd3 | ||
|
|
905d71c9e3 | ||
|
|
bf430ad14b | ||
|
|
a58d8fc68b | ||
|
|
3c41c0c46e | ||
|
|
6ffc53b290 | ||
|
|
8f807a3006 | ||
|
|
34c694c20e | ||
|
|
3ca139e21e | ||
|
|
a8a895a61b | ||
|
|
36361d623d | ||
|
|
652e0d45a9 | ||
|
|
556901ea48 | ||
|
|
05255b9c3f | ||
|
|
11e1b8a19d | ||
|
|
37d8cd7b75 | ||
|
|
ca973b68e0 | ||
|
|
d19a8ec7da | ||
|
|
4152ac4aa2 | ||
|
|
efdc7042df | ||
|
|
ebf4be3711 | ||
|
|
16d72d2351 | ||
|
|
b2210f429e | ||
|
|
7410bc90f0 | ||
|
|
d8a7c547df | ||
|
|
ecaf0189cc | ||
|
|
ca5f470956 | ||
|
|
3ba19c52d5 | ||
|
|
6734c966b3 | ||
|
|
5e2296f2a4 | ||
|
|
c5228cbc64 | ||
|
|
7e15f179c6 | ||
|
|
321eb2ec6f | ||
|
|
2ee73ca911 | ||
|
|
19a529e917 | ||
|
|
7f065e38a7 | ||
|
|
c4a4802a8c | ||
|
|
44e4f8d1ba | ||
|
|
eaf525d41d | ||
|
|
6d7dbe5536 | ||
|
|
ab397e2b1a | ||
|
|
6be81feb2d | ||
|
|
9b1a75a74b | ||
|
|
8792fd22b9 | ||
|
|
5dd0193ba6 | ||
|
|
b159e8acee | ||
|
|
a99c8eb6c6 | ||
|
|
4218b31e7b | ||
|
|
35bae1eef2 | ||
|
|
d119610cf1 | ||
|
|
4aed41cbe8 | ||
|
|
c462292e4d | ||
|
|
b04e7bba9f | ||
|
|
e6364b4ff6 | ||
|
|
36b9c0a946 | ||
|
|
9086119082 | ||
|
|
dc94079d74 | ||
|
|
78c27b99bd | ||
|
|
f054e9ee54 | ||
|
|
205e83a6d5 | ||
|
|
5063464d5e | ||
|
|
38af04c6ce | ||
|
|
03225cf20f | ||
|
|
3682080da2 | ||
|
|
13cb9cb07b | ||
|
|
05204a982e | ||
|
|
18b288dcfe | ||
|
|
60d7e32f81 | ||
|
|
6a5c7ef43f | ||
|
|
e5c4bba906 | ||
|
|
e07fb24987 | ||
|
|
e7b84432f9 | ||
|
|
b0e062b2f8 | ||
|
|
259121c7a7 | ||
|
|
326241d9d8 | ||
|
|
7c7da9df05 | ||
|
|
cf3f1c3081 | ||
|
|
f00d5cb8ca | ||
|
|
3920de7119 | ||
|
|
fd409a16a1 | ||
|
|
7145afe729 | ||
|
|
70760b5d3b | ||
|
|
d418355d4d | ||
|
|
81ba666db7 | ||
|
|
a147401034 | ||
|
|
36e9f523d1 | ||
|
|
ae257651bf | ||
|
|
53cc3262bd | ||
|
|
2e5b4946e1 | ||
|
|
67c49a7662 | ||
|
|
d06807c634 | ||
|
|
2321603eb7 | ||
|
|
ba20ffdde7 | ||
|
|
cf8907ed0f | ||
|
|
95176b0666 | ||
|
|
f0d9844dfb | ||
|
|
54f8f1223f | ||
|
|
339a839dbe | ||
|
|
e2e10b91a7 | ||
|
|
a9d242a213 | ||
|
|
49581a4a2a | ||
|
|
d8a11fd706 | ||
|
|
d63cf94d6d | ||
|
|
7d8a309017 | ||
|
|
99eeb01525 | ||
|
|
92b07ba8d1 | ||
|
|
c2b06b9e55 | ||
|
|
dd67192057 | ||
|
|
7937064fb7 | ||
|
|
0762c7caef | ||
|
|
d333593aa6 | ||
|
|
4e03176634 | ||
|
|
e20e0425b1 | ||
|
|
228b030c82 | ||
|
|
7a979e9f72 | ||
|
|
25c4c9b63c | ||
|
|
03970764d8 | ||
|
|
168e1f0e2d | ||
|
|
d3386907a4 | ||
|
|
de3c76983a | ||
|
|
b9d8789771 | ||
|
|
b186b27600 | ||
|
|
23c39ebefd | ||
|
|
ff83efe376 | ||
|
|
17ba813a6d | ||
|
|
78dd010a04 | ||
|
|
88021ba404 | ||
|
|
491b3d707c | ||
|
|
7d8ca2010b | ||
|
|
dbef8f0b78 | ||
|
|
ed85e368e3 | ||
|
|
4867ed23dc | ||
|
|
9f35d4dfca | ||
|
|
b434ffba2d | ||
|
|
a60712d826 | ||
|
|
53078f3069 | ||
|
|
3416d3f5f1 | ||
|
|
b1cc9bf452 | ||
|
|
6fe6dcfac9 | ||
|
|
001515bdc4 | ||
|
|
c1aaef28a9 | ||
|
|
f7e9215f5e | ||
|
|
e2a2fe36fc | ||
|
|
3628fcf083 | ||
|
|
44cad7df30 | ||
|
|
bbd58d7357 | ||
|
|
222748dfbf | ||
|
|
a0eca9c6d1 | ||
|
|
b556b86301 | ||
|
|
e82b358831 | ||
|
|
9658f4383c | ||
|
|
f6c504610b | ||
|
|
f5c633415d | ||
|
|
c5157c1027 | ||
|
|
bba1e2adc9 | ||
|
|
a63714dc86 | ||
|
|
ab74ac8eca | ||
|
|
f19b934869 | ||
|
|
efd155dd3c | ||
|
|
14d052d242 | ||
|
|
39cee987d9 | ||
|
|
6ed765698a | ||
|
|
138350fe3d | ||
|
|
4e522448b1 | ||
|
|
8645f244da | ||
|
|
be64422d1c | ||
|
|
9751fed493 | ||
|
|
dc8c331c68 | ||
|
|
d052d45712 | ||
|
|
4242411089 | ||
|
|
f396266c74 | ||
|
|
21d8ecdacd | ||
|
|
6bdb2fe5a0 | ||
|
|
c1c23bb4b6 | ||
|
|
98fef81d19 | ||
|
|
71cab65df6 | ||
|
|
111e515da7 | ||
|
|
5b8aeafdb9 | ||
|
|
c1a6131aa8 | ||
|
|
4821858afb | ||
|
|
446390a8d1 | ||
|
|
6a665ffb84 | ||
|
|
10570f5ad6 | ||
|
|
2c1083bda1 | ||
|
|
f8a0a0ba59 | ||
|
|
1d14a17ffd | ||
|
|
a8c9303892 | ||
|
|
bf41674e06 | ||
|
|
6e6ae173fd | ||
|
|
7d5c1581f1 | ||
|
|
6d5fb49687 | ||
|
|
e96ac74b11 | ||
|
|
27b1d448a3 | ||
|
|
347ba1a2d8 | ||
|
|
32166fd56a | ||
|
|
e8173fbc16 | ||
|
|
63552abce5 | ||
|
|
16cb7388ee | ||
|
|
eacfbc048a | ||
|
|
da832dbda2 | ||
|
|
f51a3738aa | ||
|
|
6d431c3fc3 | ||
|
|
2821820281 | ||
|
|
3713dfe139 | ||
|
|
c076b805e7 | ||
|
|
7a44eee093 | ||
|
|
485979cd86 | ||
|
|
5a80d4e5ea | ||
|
|
c3d322f26c | ||
|
|
230b73d14a | ||
|
|
042f292e4f | ||
|
|
daa00bc65a | ||
|
|
1b22f2d8b8 | ||
|
|
1e672b93e7 | ||
|
|
6ee3c1b3e5 | ||
|
|
156206dfee | ||
|
|
7dcb2ae24c | ||
|
|
6b03cb913c |
91
.coveragerc
91
.coveragerc
@@ -62,6 +62,9 @@ omit =
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
homeassistant/components/daikin.py
|
||||
homeassistant/components/*/daikin.py
|
||||
|
||||
homeassistant/components/deconz/*
|
||||
homeassistant/components/*/deconz.py
|
||||
|
||||
@@ -82,12 +85,21 @@ omit =
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/egardia.py
|
||||
homeassistant/components/*/egardia.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/fritzbox.py
|
||||
homeassistant/components/*/fritzbox.py
|
||||
|
||||
homeassistant/components/eufy.py
|
||||
homeassistant/components/*/eufy.py
|
||||
|
||||
homeassistant/components/gc100.py
|
||||
homeassistant/components/*/gc100.py
|
||||
|
||||
@@ -100,9 +112,15 @@ omit =
|
||||
homeassistant/components/hive.py
|
||||
homeassistant/components/*/hive.py
|
||||
|
||||
homeassistant/components/homekit_controller/__init__.py
|
||||
homeassistant/components/*/homekit_controller.py
|
||||
|
||||
homeassistant/components/homematic/__init__.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/homematicip_cloud.py
|
||||
homeassistant/components/*/homematicip_cloud.py
|
||||
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/*/ihc.py
|
||||
|
||||
@@ -151,9 +169,6 @@ omit =
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/mercedesme.py
|
||||
homeassistant/components/*/mercedesme.py
|
||||
|
||||
homeassistant/components/mochad.py
|
||||
homeassistant/components/*/mochad.py
|
||||
|
||||
@@ -181,8 +196,11 @@ omit =
|
||||
homeassistant/components/opencv.py
|
||||
homeassistant/components/*/opencv.py
|
||||
|
||||
homeassistant/components/qwikswitch.py
|
||||
homeassistant/components/*/qwikswitch.py
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/switch/qwikswitch.py
|
||||
homeassistant/components/light/qwikswitch.py
|
||||
|
||||
homeassistant/components/rachio.py
|
||||
homeassistant/components/*/rachio.py
|
||||
@@ -244,6 +262,9 @@ omit =
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
homeassistant/components/upcloud.py
|
||||
homeassistant/components/*/upcloud.py
|
||||
|
||||
homeassistant/components/usps.py
|
||||
homeassistant/components/*/usps.py
|
||||
|
||||
@@ -274,11 +295,9 @@ omit =
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/xiaomi_aqara.py
|
||||
homeassistant/components/binary_sensor/xiaomi_aqara.py
|
||||
homeassistant/components/cover/xiaomi_aqara.py
|
||||
homeassistant/components/light/xiaomi_aqara.py
|
||||
homeassistant/components/sensor/xiaomi_aqara.py
|
||||
homeassistant/components/switch/xiaomi_aqara.py
|
||||
homeassistant/components/*/xiaomi_aqara.py
|
||||
|
||||
homeassistant/components/*/xiaomi_miio.py
|
||||
|
||||
homeassistant/components/zabbix.py
|
||||
homeassistant/components/*/zabbix.py
|
||||
@@ -293,14 +312,11 @@ omit =
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/daikin.py
|
||||
homeassistant/components/*/daikin.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/egardia.py
|
||||
homeassistant/components/alarm_control_panel/ialarm.py
|
||||
homeassistant/components/alarm_control_panel/ifttt.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
@@ -312,7 +328,6 @@ omit =
|
||||
homeassistant/components/binary_sensor/hikvision.py
|
||||
homeassistant/components/binary_sensor/iss.py
|
||||
homeassistant/components/binary_sensor/mystrom.py
|
||||
homeassistant/components/binary_sensor/pilight.py
|
||||
homeassistant/components/binary_sensor/ping.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
@@ -325,6 +340,7 @@ omit =
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/proxy.py
|
||||
homeassistant/components/camera/ring.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
@@ -345,6 +361,7 @@ omit =
|
||||
homeassistant/components/climate/touchline.py
|
||||
homeassistant/components/climate/venstar.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/gogogate2.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/knx.py
|
||||
homeassistant/components/cover/myq.py
|
||||
@@ -353,13 +370,16 @@ omit =
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/automatic.py
|
||||
homeassistant/components/device_tracker/bbox.py
|
||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/google_maps.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/hitron_coda.py
|
||||
homeassistant/components/device_tracker/huawei_router.py
|
||||
@@ -386,30 +406,31 @@ omit =
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/fan/xiaomi_miio.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/folder_watcher.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/goalfeed.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/image_processing/dlib_face_detect.py
|
||||
homeassistant/components/image_processing/dlib_face_identify.py
|
||||
homeassistant/components/image_processing/seven_segments.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/avion.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/blinkt.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/decora_wifi.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/greenwave.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/iglo.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/nanoleaf_aurora.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
@@ -418,7 +439,6 @@ omit =
|
||||
homeassistant/components/light/tplink.py
|
||||
homeassistant/components/light/tradfri.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/xiaomi_miio.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/zengge.py
|
||||
@@ -427,12 +447,14 @@ omit =
|
||||
homeassistant/components/lock/nello.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/map.py
|
||||
homeassistant/components/media_extractor.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/bluesound.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/channels.py
|
||||
homeassistant/components/media_player/clementine.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
@@ -464,6 +486,7 @@ omit =
|
||||
homeassistant/components/media_player/russound_rio.py
|
||||
homeassistant/components/media_player/russound_rnet.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/songpal.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
@@ -472,8 +495,8 @@ omit =
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/xiaomi_tv.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/media_player/yamaha_musiccast.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/media_player/ziggo_mediabox_xl.py
|
||||
homeassistant/components/mycroft.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
@@ -481,8 +504,8 @@ omit =
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/clickatell.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
@@ -492,6 +515,7 @@ omit =
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/mastodon.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/mycroft.py
|
||||
@@ -508,6 +532,8 @@ omit =
|
||||
homeassistant/components/notify/simplepush.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/stride.py
|
||||
homeassistant/components/notify/synology_chat.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/telstra.py
|
||||
@@ -520,7 +546,6 @@ omit =
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/remote/xiaomi_miio.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
@@ -543,7 +568,6 @@ omit =
|
||||
homeassistant/components/sensor/crimereports.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deluge.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
@@ -565,6 +589,7 @@ omit =
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
homeassistant/components/sensor/foobot.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gearbest.py
|
||||
@@ -577,8 +602,8 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/irish_rail_transport.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
@@ -619,10 +644,14 @@ omit =
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/sht31.py
|
||||
homeassistant/components/sensor/shodan.py
|
||||
homeassistant/components/sensor/sigfox.py
|
||||
homeassistant/components/sensor/simulated.py
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
homeassistant/components/sensor/sma.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
@@ -634,30 +663,34 @@ omit =
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/syncthru.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/sytadin.py
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/teksavvy.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/tibber.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/trafikverket_weatherstation.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/travisci.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/uscis.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/viaggiatreno.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/waze_travel_time.py
|
||||
homeassistant/components/sensor/whois.py
|
||||
homeassistant/components/sensor/worldtidesinfo.py
|
||||
homeassistant/components/sensor/worxlandroid.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/sensor/zestimate.py
|
||||
homeassistant/components/shiftr.py
|
||||
homeassistant/components/spc.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
@@ -675,7 +708,6 @@ omit =
|
||||
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/rainbird.py
|
||||
homeassistant/components/switch/rainmachine.py
|
||||
@@ -685,7 +717,7 @@ omit =
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/xiaomi_miio.py
|
||||
homeassistant/components/switch/vesync.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
@@ -694,7 +726,6 @@ omit =
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
homeassistant/components/vacuum/roomba.py
|
||||
homeassistant/components/vacuum/xiaomi_miio.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
homeassistant/components/weather/darksky.py
|
||||
|
||||
38
.github/ISSUE_TEMPLATE.md
vendored
38
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,35 +1,45 @@
|
||||
Make sure you are running the latest version of Home Assistant before reporting an issue.
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||
-->
|
||||
|
||||
You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum:
|
||||
|
||||
**Home Assistant release (`hass --version`):**
|
||||
**Home Assistant release with the issue:**
|
||||
<!--
|
||||
- Frontend -> Developer tools -> Info
|
||||
- Or use this command: hass --version
|
||||
-->
|
||||
|
||||
|
||||
**Python release (`python3 --version`):**
|
||||
**Last working Home Assistant release (if known):**
|
||||
|
||||
|
||||
**Operating environment (Hass.io/Docker/Windows/etc.):**
|
||||
<!--
|
||||
Please provide details about your environment.
|
||||
-->
|
||||
|
||||
**Component/platform:**
|
||||
<!--
|
||||
Please add the link to the documentation at https://www.home-assistant.io/components/ of the component/platform in question.
|
||||
-->
|
||||
|
||||
|
||||
**Description of problem:**
|
||||
|
||||
|
||||
**Expected:**
|
||||
|
||||
|
||||
**Problem-relevant `configuration.yaml` entries and steps to reproduce:**
|
||||
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Traceback (if applicable):**
|
||||
```bash
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
**Additional info:**
|
||||
**Additional information:**
|
||||
|
||||
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -12,19 +12,18 @@
|
||||
|
||||
## Checklist:
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
|
||||
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**
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
- [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New files were added to `.coveragerc`.
|
||||
|
||||
If the code does not interact with devices:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -21,6 +21,7 @@ Icon
|
||||
*.iml
|
||||
|
||||
# pytest
|
||||
.pytest_cache
|
||||
.cache
|
||||
|
||||
# GITHUB Proposed Python stuff:
|
||||
@@ -103,3 +104,6 @@ desktop.ini
|
||||
|
||||
# mypy
|
||||
/.mypy_cache/*
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
|
||||
0
.gitmodules
vendored
0
.gitmodules
vendored
17
.travis.yml
17
.travis.yml
@@ -6,12 +6,10 @@ addons:
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.4.2"
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=lint
|
||||
- python: "3.4.2"
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=pylint
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=py34
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
@@ -30,4 +28,15 @@ cache:
|
||||
install: pip install -U tox coveralls
|
||||
language: python
|
||||
script: travis_wait 30 tox --develop
|
||||
services:
|
||||
- docker
|
||||
before_deploy:
|
||||
- docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21
|
||||
deploy:
|
||||
skip_cleanup: true
|
||||
provider: script
|
||||
script: script/travis_deploy
|
||||
on:
|
||||
branch: dev
|
||||
condition: $TOXENV = lint
|
||||
after_success: coveralls
|
||||
|
||||
27
CODEOWNERS
Executable file → Normal file
27
CODEOWNERS
Executable file → Normal file
@@ -29,9 +29,6 @@ homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/websocket_api.py @home-assistant/core
|
||||
homeassistant/components/zone.py @home-assistant/core
|
||||
|
||||
# To monitor non-pypi additions
|
||||
requirements_all.txt @andrey-git
|
||||
|
||||
# HomeAssistant developer Teams
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
@@ -43,17 +40,21 @@ homeassistant/components/hassio.py @home-assistant/hassio
|
||||
|
||||
# Individual components
|
||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
||||
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/group.py @cdce8p
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/media_player/emby.py @mezz64
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/mediaroom.py @dgomes
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
@@ -62,13 +63,17 @@ homeassistant/components/media_player/xiaomi_tv.py @fattdev
|
||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/plant.py @ChristianKuehnel
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/filter.py @dgomes
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/sma.py @kellerza
|
||||
homeassistant/components/sensor/sql.py @dgomes
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/upnp.py @dgomes
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/switch/rainmachine.py @bachya
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
@@ -77,15 +82,17 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/axis.py @kane610
|
||||
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/eight_sleep.py @mezz64
|
||||
homeassistant/components/*/eight_sleep.py @mezz64
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
homeassistant/components/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
@@ -93,5 +100,9 @@ homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tradfri.py @ggravlingen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||
|
||||
homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
@@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) 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.
|
||||
|
||||
@@ -4,10 +4,10 @@ homeassistant.util package
|
||||
Submodules
|
||||
----------
|
||||
|
||||
homeassistant.util.async module
|
||||
homeassistant.util.async_ module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: homeassistant.util.async
|
||||
.. automodule:: homeassistant.util.async_
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -15,7 +15,6 @@ from homeassistant.const import (
|
||||
__version__,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
REQUIRED_PYTHON_VER,
|
||||
REQUIRED_PYTHON_VER_WIN,
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
|
||||
@@ -33,12 +32,7 @@ def attempt_use_uvloop():
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate that the right Python version is running."""
|
||||
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:
|
||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER))
|
||||
sys.exit(1)
|
||||
@@ -132,6 +126,10 @@ def get_arguments() -> argparse.Namespace:
|
||||
default=None,
|
||||
help='Log file to write to. If not set, CONFIG/home-assistant.log '
|
||||
'is used')
|
||||
parser.add_argument(
|
||||
'--log-no-color',
|
||||
action='store_true',
|
||||
help="Disable color logs")
|
||||
parser.add_argument(
|
||||
'--runner',
|
||||
action='store_true',
|
||||
@@ -265,20 +263,21 @@ def setup_and_run_hass(config_dir: str,
|
||||
hass = bootstrap.from_config_dict(
|
||||
config, config_dir=config_dir, verbose=args.verbose,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file)
|
||||
log_file=args.log_file, log_no_color=args.log_no_color)
|
||||
else:
|
||||
config_file = ensure_config_file(config_dir)
|
||||
print('Config directory:', config_dir)
|
||||
hass = bootstrap.from_config_file(
|
||||
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
log_rotate_days=args.log_rotate_days, log_file=args.log_file)
|
||||
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
|
||||
log_no_color=args.log_no_color)
|
||||
|
||||
if hass is None:
|
||||
return None
|
||||
|
||||
if args.open_ui:
|
||||
# Imported here to avoid importing asyncio before monkey patch
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
def open_browser(event):
|
||||
"""Open the webinterface in a browser."""
|
||||
@@ -341,7 +340,8 @@ def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
if os.environ.get('HASS_NO_MONKEY') != '1':
|
||||
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
|
||||
if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1':
|
||||
if sys.version_info[:2] >= (3, 6):
|
||||
monkey_patch.disable_c_asyncio()
|
||||
monkey_patch.patch_weakref_tasks()
|
||||
|
||||
@@ -42,7 +42,8 @@ def from_config_dict(config: Dict[str, Any],
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = False,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None) \
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
@@ -60,7 +61,7 @@ def from_config_dict(config: Dict[str, Any],
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days, log_file)
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
)
|
||||
|
||||
return hass
|
||||
@@ -74,7 +75,8 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = False,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None) \
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
@@ -84,15 +86,8 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
start = time()
|
||||
|
||||
if enable_log:
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
|
||||
if sys.version_info[:2] < (3, 5):
|
||||
_LOGGER.warning(
|
||||
'Python 3.4 support has been deprecated and will be removed in '
|
||||
'the beginning of 2018. Please upgrade Python or your operating '
|
||||
'system. More info: https://home-assistant.io/blog/2017/10/06/'
|
||||
'deprecating-python-3.4-support/'
|
||||
)
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
@@ -112,17 +107,17 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
if not loader.PREPARED:
|
||||
yield from hass.async_add_job(loader.prepare, hass)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
config = OrderedDict(config)
|
||||
|
||||
# Merge packages
|
||||
conf_util.merge_packages_config(
|
||||
config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
# Use OrderedDict in case original one was one.
|
||||
# Convert values to dictionaries if they are None
|
||||
new_config = OrderedDict()
|
||||
# Ensure we have no None values after merge
|
||||
for key, value in config.items():
|
||||
new_config[key] = value or {}
|
||||
config = new_config
|
||||
if not value:
|
||||
config[key] = {}
|
||||
|
||||
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
||||
yield from hass.config_entries.async_load()
|
||||
@@ -172,7 +167,8 @@ def from_config_file(config_path: str,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None):
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
@@ -184,7 +180,8 @@ def from_config_file(config_path: str,
|
||||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days, log_file)
|
||||
config_path, hass, verbose, skip_pip,
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
)
|
||||
|
||||
return hass
|
||||
@@ -196,7 +193,8 @@ def async_from_config_file(config_path: str,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None):
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter.
|
||||
@@ -207,7 +205,8 @@ def async_from_config_file(config_path: str,
|
||||
hass.config.config_dir = config_dir
|
||||
yield from async_mount_local_lib_path(config_dir, hass.loop)
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
|
||||
try:
|
||||
config_dict = yield from hass.async_add_job(
|
||||
@@ -224,40 +223,51 @@ def async_from_config_file(config_path: str,
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
|
||||
log_rotate_days=None, log_file=None) -> None:
|
||||
def async_enable_logging(hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
log_rotate_days=None,
|
||||
log_file=None,
|
||||
log_no_color: bool = False) -> None:
|
||||
"""Set up the logging.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
"[%(name)s] %(message)s")
|
||||
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
|
||||
datefmt = '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
if not log_no_color:
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
# basicConfig must be called after importing colorlog in order to
|
||||
# ensure that the handlers it sets up wraps the correct streams.
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
colorfmt,
|
||||
datefmt=datefmt,
|
||||
reset=True,
|
||||
log_colors={
|
||||
'DEBUG': 'cyan',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'red',
|
||||
}
|
||||
))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# If the above initialization failed for any reason, setup the default
|
||||
# formatting. If the above succeeds, this wil result in a no-op.
|
||||
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
|
||||
|
||||
# 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(
|
||||
colorfmt,
|
||||
datefmt=datefmt,
|
||||
reset=True,
|
||||
log_colors={
|
||||
'DEBUG': 'cyan',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'red',
|
||||
}
|
||||
))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
if log_file is None:
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
|
||||
@@ -156,9 +156,10 @@ def async_setup(hass, config):
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned on {}"))
|
||||
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned off {}"))
|
||||
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF,
|
||||
"Turned {} off"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.12.2']
|
||||
REQUIREMENTS = ['abodepy==0.13.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,6 +27,7 @@ CONF_ATTRIBUTION = "Data provided by goabode.com"
|
||||
CONF_POLLING = 'polling'
|
||||
|
||||
DOMAIN = 'abode'
|
||||
DEFAULT_CACHEDB = './abodepy_cache.pickle'
|
||||
|
||||
NOTIFICATION_ID = 'abode_notification'
|
||||
NOTIFICATION_TITLE = 'Abode Security Setup'
|
||||
@@ -87,12 +88,13 @@ ABODE_PLATFORMS = [
|
||||
class AbodeSystem(object):
|
||||
"""Abode System class."""
|
||||
|
||||
def __init__(self, username, password, name, polling, exclude, lights):
|
||||
def __init__(self, username, password, cache,
|
||||
name, polling, exclude, lights):
|
||||
"""Initialize the system."""
|
||||
import abodepy
|
||||
self.abode = abodepy.Abode(
|
||||
username, password, auto_login=True, get_devices=True,
|
||||
get_automations=True)
|
||||
get_automations=True, cache_path=cache)
|
||||
self.name = name
|
||||
self.polling = polling
|
||||
self.exclude = exclude
|
||||
@@ -129,8 +131,9 @@ def setup(hass, config):
|
||||
lights = conf.get(CONF_LIGHTS)
|
||||
|
||||
try:
|
||||
cache = hass.config.path(DEFAULT_CACHEDB)
|
||||
hass.data[DOMAIN] = AbodeSystem(
|
||||
username, password, name, polling, exclude, lights)
|
||||
username, password, cache, name, polling, exclude, lights)
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.0']
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=1)
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
|
||||
@@ -4,130 +4,70 @@ Interfaces with Egardia/Woonveilig alarm control panel.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.egardia/
|
||||
"""
|
||||
import asyncio
|
||||
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_PASSWORD, CONF_PORT, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN)
|
||||
import homeassistant.exceptions as exc
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.26']
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED,
|
||||
STATE_ALARM_ARMED_NIGHT)
|
||||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE, EGARDIA_SERVER,
|
||||
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
|
||||
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
|
||||
)
|
||||
DEPENDENCIES = ['egardia']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_REPORT_SERVER_CODES = 'report_server_codes'
|
||||
CONF_REPORT_SERVER_ENABLED = 'report_server_enabled'
|
||||
CONF_REPORT_SERVER_PORT = 'report_server_port'
|
||||
CONF_REPORT_SERVER_CODES_IGNORE = 'ignore'
|
||||
CONF_VERSION = 'version'
|
||||
|
||||
DEFAULT_NAME = 'Egardia'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_REPORT_SERVER_ENABLED = False
|
||||
DEFAULT_REPORT_SERVER_PORT = 52010
|
||||
DEFAULT_VERSION = 'GATE-01'
|
||||
DOMAIN = 'egardia'
|
||||
D_EGARDIASRV = 'egardiaserver'
|
||||
|
||||
NOTIFICATION_ID = 'egardia_notification'
|
||||
NOTIFICATION_TITLE = 'Egardia'
|
||||
|
||||
STATES = {
|
||||
'ARM': STATE_ALARM_ARMED_AWAY,
|
||||
'DAY HOME': STATE_ALARM_ARMED_HOME,
|
||||
'DISARM': STATE_ALARM_DISARMED,
|
||||
'ARMHOME': STATE_ALARM_ARMED_HOME,
|
||||
'HOME': STATE_ALARM_ARMED_HOME,
|
||||
'TRIGGERED': STATE_ALARM_TRIGGERED,
|
||||
'UNKNOWN': STATE_UNKNOWN,
|
||||
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
|
||||
'TRIGGERED': STATE_ALARM_TRIGGERED
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_REPORT_SERVER_CODES): vol.All(cv.ensure_list),
|
||||
vol.Optional(CONF_REPORT_SERVER_ENABLED,
|
||||
default=DEFAULT_REPORT_SERVER_ENABLED): cv.boolean,
|
||||
vol.Optional(CONF_REPORT_SERVER_PORT, default=DEFAULT_REPORT_SERVER_PORT):
|
||||
cv.port,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Egardia platform."""
|
||||
from pythonegardia import egardiadevice
|
||||
from pythonegardia import egardiaserver
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
rs_enabled = config.get(CONF_REPORT_SERVER_ENABLED)
|
||||
rs_port = config.get(CONF_REPORT_SERVER_PORT)
|
||||
rs_codes = config.get(CONF_REPORT_SERVER_CODES)
|
||||
version = config.get(CONF_VERSION)
|
||||
|
||||
try:
|
||||
egardiasystem = egardiadevice.EgardiaDevice(
|
||||
host, port, username, password, '', version)
|
||||
except requests.exceptions.RequestException:
|
||||
raise exc.PlatformNotReady()
|
||||
except egardiadevice.UnauthorizedError:
|
||||
_LOGGER.error("Unable to authorize. Wrong password or username")
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
eg_dev = EgardiaAlarm(
|
||||
name, egardiasystem, rs_enabled, rs_codes)
|
||||
|
||||
if rs_enabled:
|
||||
# Set up the egardia server
|
||||
_LOGGER.info("Setting up EgardiaServer")
|
||||
try:
|
||||
if D_EGARDIASRV not in hass.data:
|
||||
server = egardiaserver.EgardiaServer('', rs_port)
|
||||
bound = server.bind()
|
||||
if not bound:
|
||||
raise IOError(
|
||||
"Binding error occurred while starting EgardiaServer")
|
||||
hass.data[D_EGARDIASRV] = server
|
||||
server.start()
|
||||
except IOError:
|
||||
return
|
||||
hass.data[D_EGARDIASRV].register_callback(eg_dev.handle_status_event)
|
||||
|
||||
def handle_stop_event(event):
|
||||
"""Call function for Home Assistant stop event."""
|
||||
hass.data[D_EGARDIASRV].stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event)
|
||||
|
||||
add_devices([eg_dev], True)
|
||||
device = EgardiaAlarm(
|
||||
discovery_info['name'],
|
||||
hass.data[EGARDIA_DEVICE],
|
||||
discovery_info[CONF_REPORT_SERVER_ENABLED],
|
||||
discovery_info.get(CONF_REPORT_SERVER_CODES),
|
||||
discovery_info[CONF_REPORT_SERVER_PORT])
|
||||
# add egardia alarm device
|
||||
add_devices([device], True)
|
||||
|
||||
|
||||
class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a Egardia alarm."""
|
||||
|
||||
def __init__(self, name, egardiasystem, rs_enabled=False, rs_codes=None):
|
||||
def __init__(self, name, egardiasystem,
|
||||
rs_enabled=False, rs_codes=None, rs_port=52010):
|
||||
"""Initialize the Egardia alarm."""
|
||||
self._name = name
|
||||
self._egardiasystem = egardiasystem
|
||||
self._status = None
|
||||
self._rs_enabled = rs_enabled
|
||||
if rs_codes is not None:
|
||||
self._rs_codes = rs_codes[0]
|
||||
else:
|
||||
self._rs_codes = rs_codes
|
||||
self._rs_codes = rs_codes
|
||||
self._rs_port = rs_port
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add Egardiaserver callback if enabled."""
|
||||
if self._rs_enabled:
|
||||
_LOGGER.debug("Registering callback to Egardiaserver")
|
||||
self.hass.data[EGARDIA_SERVER].register_callback(
|
||||
self.handle_status_event)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -156,31 +96,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def lookupstatusfromcode(self, statuscode):
|
||||
"""Look at the rs_codes and returns the status from the code."""
|
||||
status = 'UNKNOWN'
|
||||
if self._rs_codes is not None:
|
||||
statuscode = str(statuscode).strip()
|
||||
for i in self._rs_codes:
|
||||
val = str(self._rs_codes[i]).strip()
|
||||
if ',' in val:
|
||||
splitted = val.split(',')
|
||||
for code in splitted:
|
||||
code = str(code).strip()
|
||||
if statuscode == code:
|
||||
status = i.upper()
|
||||
break
|
||||
elif statuscode == val:
|
||||
status = i.upper()
|
||||
break
|
||||
status = next((
|
||||
status_group.upper() for status_group, codes
|
||||
in self._rs_codes.items() for code in codes
|
||||
if statuscode == code), 'UNKNOWN')
|
||||
return status
|
||||
|
||||
def parsestatus(self, status):
|
||||
"""Parse the status."""
|
||||
_LOGGER.debug("Parsing status %s", status)
|
||||
# Ignore the statuscode if it is IGNORE
|
||||
if status.lower().strip() != CONF_REPORT_SERVER_CODES_IGNORE:
|
||||
_LOGGER.debug("Not ignoring status")
|
||||
newstatus = ([v for k, v in STATES.items()
|
||||
if status.upper() == k][0])
|
||||
if status.lower().strip() != REPORT_SERVER_CODES_IGNORE:
|
||||
_LOGGER.debug("Not ignoring status %s", status)
|
||||
newstatus = STATES.get(status.upper())
|
||||
_LOGGER.debug("newstatus %s", newstatus)
|
||||
self._status = newstatus
|
||||
else:
|
||||
_LOGGER.error("Ignoring status")
|
||||
|
||||
170
homeassistant/components/alarm_control_panel/ifttt.py
Normal file
170
homeassistant/components/alarm_control_panel/ifttt.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Interfaces with alarm control panels that have to be controlled through IFTTT.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.ifttt/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
DOMAIN, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.ifttt import (
|
||||
ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE,
|
||||
CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['ifttt']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_STATES = [
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME]
|
||||
|
||||
DATA_IFTTT_ALARM = 'ifttt_alarm'
|
||||
DEFAULT_NAME = "Home"
|
||||
|
||||
CONF_EVENT_AWAY = "event_arm_away"
|
||||
CONF_EVENT_HOME = "event_arm_home"
|
||||
CONF_EVENT_NIGHT = "event_arm_night"
|
||||
CONF_EVENT_DISARM = "event_disarm"
|
||||
|
||||
DEFAULT_EVENT_AWAY = "alarm_arm_away"
|
||||
DEFAULT_EVENT_HOME = "alarm_arm_home"
|
||||
DEFAULT_EVENT_NIGHT = "alarm_arm_night"
|
||||
DEFAULT_EVENT_DISARM = "alarm_disarm"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string,
|
||||
vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string,
|
||||
vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string,
|
||||
vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state"
|
||||
|
||||
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_STATE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a control panel managed through IFTTT."""
|
||||
if DATA_IFTTT_ALARM not in hass.data:
|
||||
hass.data[DATA_IFTTT_ALARM] = []
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
event_away = config.get(CONF_EVENT_AWAY)
|
||||
event_home = config.get(CONF_EVENT_HOME)
|
||||
event_night = config.get(CONF_EVENT_NIGHT)
|
||||
event_disarm = config.get(CONF_EVENT_DISARM)
|
||||
optimistic = config.get(CONF_OPTIMISTIC)
|
||||
|
||||
alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home,
|
||||
event_night, event_disarm, optimistic)
|
||||
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
|
||||
add_devices([alarmpanel])
|
||||
|
||||
async def push_state_update(service):
|
||||
"""Set the service state as device state attribute."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
state = service.data.get(ATTR_STATE)
|
||||
devices = hass.data[DATA_IFTTT_ALARM]
|
||||
if entity_ids:
|
||||
devices = [d for d in devices if d.entity_id in entity_ids]
|
||||
|
||||
for device in devices:
|
||||
device.push_alarm_state(state)
|
||||
device.async_schedule_update_ha_state()
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update,
|
||||
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA)
|
||||
|
||||
|
||||
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an alarm control panel controlled through IFTTT."""
|
||||
|
||||
def __init__(self, name, code, event_away, event_home, event_night,
|
||||
event_disarm, optimistic):
|
||||
"""Initialize the alarm control panel."""
|
||||
self._name = name
|
||||
self._code = code
|
||||
self._event_away = event_away
|
||||
self._event_home = event_home
|
||||
self._event_night = event_night
|
||||
self._event_disarm = event_disarm
|
||||
self._optimistic = optimistic
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Notify that this platform return an assumed state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._check_code(code):
|
||||
return
|
||||
self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._check_code(code):
|
||||
return
|
||||
self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._check_code(code):
|
||||
return
|
||||
self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
if not self._check_code(code):
|
||||
return
|
||||
self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def set_alarm_state(self, event, state):
|
||||
"""Call the IFTTT trigger service to change the alarm state."""
|
||||
data = {ATTR_EVENT: event}
|
||||
|
||||
self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data)
|
||||
_LOGGER.debug("Called IFTTT component to trigger event %s", event)
|
||||
if self._optimistic:
|
||||
self._state = state
|
||||
|
||||
def push_alarm_state(self, value):
|
||||
"""Push the alarm state to the given value."""
|
||||
if value in ALLOWED_STATES:
|
||||
_LOGGER.debug("Pushed the alarm state to %s", value)
|
||||
self._state = value
|
||||
|
||||
def _check_code(self, code):
|
||||
return self._code is None or self._code == code
|
||||
@@ -69,3 +69,13 @@ alarmdecoder_alarm_toggle_chime:
|
||||
code:
|
||||
description: A required code to toggle the alarm control panel chime with.
|
||||
example: 1234
|
||||
|
||||
ifttt_push_alarm_state:
|
||||
description: Update the alarm state to the specified value.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel which state has to be updated.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
state:
|
||||
description: The state to which the alarm control panel has to be set.
|
||||
example: 'armed_night'
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.16']
|
||||
REQUIREMENTS = ['total_connect_client==0.17']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -6,18 +6,20 @@ from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from homeassistant.components import (
|
||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
||||
alert, automation, cover, climate, fan, group, input_boolean, light, lock,
|
||||
media_player, scene, script, switch, http, sensor)
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
|
||||
SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
||||
|
||||
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -34,6 +36,16 @@ API_TEMP_UNITS = {
|
||||
TEMP_CELSIUS: 'CELSIUS',
|
||||
}
|
||||
|
||||
API_THERMOSTAT_MODES = {
|
||||
climate.STATE_HEAT: 'HEAT',
|
||||
climate.STATE_COOL: 'COOL',
|
||||
climate.STATE_AUTO: 'AUTO',
|
||||
climate.STATE_ECO: 'ECO',
|
||||
climate.STATE_IDLE: 'OFF',
|
||||
climate.STATE_FAN_ONLY: 'OFF',
|
||||
climate.STATE_DRY: 'OFF',
|
||||
}
|
||||
|
||||
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
||||
|
||||
CONF_DESCRIPTION = 'description'
|
||||
@@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface):
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp = self.entity.state
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
temp = self.entity.attributes.get(
|
||||
climate.ATTR_CURRENT_TEMPERATURE)
|
||||
return {
|
||||
'value': float(self.entity.state),
|
||||
'value': float(temp),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
}
|
||||
|
||||
|
||||
class _AlexaThermostatController(_AlexaInterface):
|
||||
def name(self):
|
||||
return 'Alexa.ThermostatController'
|
||||
|
||||
def properties_supported(self):
|
||||
properties = []
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
|
||||
properties.append({'name': 'targetSetpoint'})
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
|
||||
properties.append({'name': 'lowerSetpoint'})
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
|
||||
properties.append({'name': 'upperSetpoint'})
|
||||
if supported & climate.SUPPORT_OPERATION_MODE:
|
||||
properties.append({'name': 'thermostatMode'})
|
||||
return properties
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
if name == 'thermostatMode':
|
||||
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
|
||||
mode = API_THERMOSTAT_MODES.get(ha_mode)
|
||||
if mode is None:
|
||||
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
|
||||
self.entity.entity_id, type(self.entity),
|
||||
climate.ATTR_OPERATION_MODE, ha_mode)
|
||||
raise _UnsupportedProperty(name)
|
||||
return mode
|
||||
|
||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp = None
|
||||
if name == 'targetSetpoint':
|
||||
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
|
||||
elif name == 'lowerSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
|
||||
elif name == 'upperSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
|
||||
if temp is None:
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
return {
|
||||
'value': float(temp),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
}
|
||||
|
||||
@@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity):
|
||||
return [_AlexaPowerController(self.entity)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
||||
class _ClimateCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.THERMOSTAT]
|
||||
|
||||
def interfaces(self):
|
||||
yield _AlexaThermostatController(self.entity)
|
||||
yield _AlexaTemperatureSensor(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||
class _CoverCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
@@ -438,9 +512,7 @@ class _LightCapabilities(_AlexaEntity):
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & light.SUPPORT_BRIGHTNESS:
|
||||
yield _AlexaBrightnessController(self.entity)
|
||||
if supported & light.SUPPORT_RGB_COLOR:
|
||||
yield _AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_XY_COLOR:
|
||||
if supported & light.SUPPORT_COLOR:
|
||||
yield _AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_COLOR_TEMP:
|
||||
yield _AlexaColorTemperatureController(self.entity)
|
||||
@@ -684,17 +756,26 @@ def api_message(request,
|
||||
return response
|
||||
|
||||
|
||||
def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
|
||||
def api_error(request,
|
||||
namespace='Alexa',
|
||||
error_type='INTERNAL_ERROR',
|
||||
error_message="",
|
||||
payload=None):
|
||||
"""Create a API formatted error response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
payload = {
|
||||
'type': error_type,
|
||||
'message': error_message,
|
||||
}
|
||||
payload = payload or {}
|
||||
payload['type'] = error_type
|
||||
payload['message'] = error_message
|
||||
|
||||
return api_message(request, name='ErrorResponse', payload=payload)
|
||||
_LOGGER.info("Request %s/%s error %s: %s",
|
||||
request[API_HEADER]['namespace'],
|
||||
request[API_HEADER]['name'],
|
||||
error_type, error_message)
|
||||
|
||||
return api_message(
|
||||
request, name='ErrorResponse', namespace=namespace, payload=payload)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||
@@ -842,25 +923,16 @@ def async_api_adjust_brightness(hass, config, request, entity):
|
||||
@asyncio.coroutine
|
||||
def async_api_set_color(hass, config, request, entity):
|
||||
"""Process a set color request."""
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
rgb = color_util.color_hsb_to_RGB(
|
||||
float(request[API_PAYLOAD]['color']['hue']),
|
||||
float(request[API_PAYLOAD]['color']['saturation']),
|
||||
float(request[API_PAYLOAD]['color']['brightness'])
|
||||
)
|
||||
|
||||
if supported & light.SUPPORT_RGB_COLOR > 0:
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=False)
|
||||
else:
|
||||
xyz = color_util.color_RGB_to_xy(*rgb)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
|
||||
light.ATTR_BRIGHTNESS: xyz[2],
|
||||
}, blocking=False)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -1115,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity):
|
||||
else:
|
||||
msg = 'failed to map input {} to a media source on {}'.format(
|
||||
media_input, entity.entity_id)
|
||||
_LOGGER.error(msg)
|
||||
return api_error(
|
||||
request, error_type='INVALID_VALUE', error_message=msg)
|
||||
|
||||
@@ -1287,6 +1358,150 @@ def async_api_previous(hass, config, request, entity):
|
||||
return api_message(request)
|
||||
|
||||
|
||||
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
|
||||
"""Create temperature value out of range API error response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
temp_range = {
|
||||
'minimumValue': {
|
||||
'value': min_temp,
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
},
|
||||
'maximumValue': {
|
||||
'value': max_temp,
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
},
|
||||
}
|
||||
|
||||
msg = 'The requested temperature {} is out of range'.format(temp)
|
||||
return api_error(
|
||||
request,
|
||||
error_type='TEMPERATURE_VALUE_OUT_OF_RANGE',
|
||||
error_message=msg,
|
||||
payload={'validRange': temp_range},
|
||||
)
|
||||
|
||||
|
||||
def temperature_from_object(temp_obj, to_unit, interval=False):
|
||||
"""Get temperature from Temperature object in requested unit."""
|
||||
from_unit = TEMP_CELSIUS
|
||||
temp = float(temp_obj['value'])
|
||||
|
||||
if temp_obj['scale'] == 'FAHRENHEIT':
|
||||
from_unit = TEMP_FAHRENHEIT
|
||||
elif temp_obj['scale'] == 'KELVIN':
|
||||
# convert to Celsius if absolute temperature
|
||||
if not interval:
|
||||
temp -= 273.15
|
||||
|
||||
return convert_temperature(temp, from_unit, to_unit, interval)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
|
||||
@extract_entity
|
||||
async def async_api_set_target_temp(hass, config, request, entity):
|
||||
"""Process a set target temperature request."""
|
||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
payload = request[API_PAYLOAD]
|
||||
if 'targetSetpoint' in payload:
|
||||
temp = temperature_from_object(
|
||||
payload['targetSetpoint'], unit)
|
||||
if temp < min_temp or temp > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, temp, min_temp, max_temp, unit)
|
||||
data[ATTR_TEMPERATURE] = temp
|
||||
if 'lowerSetpoint' in payload:
|
||||
temp_low = temperature_from_object(
|
||||
payload['lowerSetpoint'], unit)
|
||||
if temp_low < min_temp or temp_low > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, temp_low, min_temp, max_temp, unit)
|
||||
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
|
||||
if 'upperSetpoint' in payload:
|
||||
temp_high = temperature_from_object(
|
||||
payload['upperSetpoint'], unit)
|
||||
if temp_high < min_temp or temp_high > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, temp_high, min_temp, max_temp, unit)
|
||||
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
|
||||
@extract_entity
|
||||
async def async_api_adjust_target_temp(hass, config, request, entity):
|
||||
"""Process an adjust target temperature request."""
|
||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
|
||||
temp_delta = temperature_from_object(
|
||||
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
|
||||
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
|
||||
|
||||
if target_temp < min_temp or target_temp > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, target_temp, min_temp, max_temp, unit)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
ATTR_TEMPERATURE: target_temp,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
|
||||
@extract_entity
|
||||
async def async_api_set_thermostat_mode(hass, config, request, entity):
|
||||
"""Process a set thermostat mode request."""
|
||||
mode = request[API_PAYLOAD]['thermostatMode']
|
||||
mode = mode if isinstance(mode, str) else mode['value']
|
||||
|
||||
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
|
||||
# Work around a pylint false positive due to
|
||||
# https://github.com/PyCQA/pylint/issues/1830
|
||||
# pylint: disable=stop-iteration-return
|
||||
ha_mode = next(
|
||||
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||
None
|
||||
)
|
||||
if ha_mode not in operation_list:
|
||||
msg = 'The requested thermostat mode {} is not supported'.format(mode)
|
||||
return api_error(
|
||||
request,
|
||||
namespace='Alexa.ThermostatController',
|
||||
error_type='UNSUPPORTED_THERMOSTAT_MODE',
|
||||
error_message=msg
|
||||
)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
climate.ATTR_OPERATION_MODE: ha_mode,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
|
||||
blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa', 'ReportState'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -10,14 +10,15 @@ from datetime import timedelta
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
from requests.exceptions import ConnectionError as ConnectError
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.1']
|
||||
REQUIREMENTS = ['amcrest==1.2.2']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -63,6 +64,12 @@ SENSORS = {
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
# Switch types are defined like: Name, icon
|
||||
SWITCHES = {
|
||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@@ -81,6 +88,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -93,14 +102,15 @@ def setup(hass, config):
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
try:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
# pylint: disable=pointless-statement
|
||||
camera.current_time
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
except (ConnectError, ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
@@ -108,12 +118,13 @@ def setup(hass, config):
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
continue
|
||||
|
||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||
name = device.get(CONF_NAME)
|
||||
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
switches = device.get(CONF_SWITCHES)
|
||||
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
|
||||
|
||||
username = device.get(CONF_USERNAME)
|
||||
@@ -143,6 +154,13 @@ def setup(hass, config):
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
if switches:
|
||||
discovery.load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SWITCHES: switches
|
||||
}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -52,9 +52,8 @@ def setup(hass, config):
|
||||
hass.http.register_view(APIComponentsView)
|
||||
hass.http.register_view(APITemplateView)
|
||||
|
||||
log_path = hass.data.get(DATA_LOGGING, None)
|
||||
if log_path:
|
||||
hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False)
|
||||
if DATA_LOGGING in hass.data:
|
||||
hass.http.register_view(APIErrorLog)
|
||||
|
||||
return True
|
||||
|
||||
@@ -131,8 +130,7 @@ class APIEventStream(HomeAssistantView):
|
||||
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()
|
||||
yield from response.write(msg.encode("UTF-8"))
|
||||
except asyncio.TimeoutError:
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
@@ -357,6 +355,17 @@ class APITemplateView(HomeAssistantView):
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
class APIErrorLog(HomeAssistantView):
|
||||
"""View to fetch the error log."""
|
||||
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error_log"
|
||||
|
||||
async def get(self, request):
|
||||
"""Retrieve API error log."""
|
||||
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
|
||||
@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_CONFIGURING = {}
|
||||
|
||||
REQUIREMENTS = ['py-august==0.3.0']
|
||||
REQUIREMENTS = ['py-august==0.4.0']
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
ACTIVITY_FETCH_LIMIT = 10
|
||||
@@ -159,7 +159,7 @@ class AugustData:
|
||||
self._api = api
|
||||
self._access_token = access_token
|
||||
self._doorbells = self._api.get_doorbells(self._access_token) or []
|
||||
self._locks = self._api.get_locks(self._access_token) or []
|
||||
self._locks = self._api.get_operable_locks(self._access_token) or []
|
||||
self._house_ids = [d.house_id for d in self._doorbells + self._locks]
|
||||
|
||||
self._doorbell_detail_by_id = {}
|
||||
|
||||
@@ -4,7 +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
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -28,6 +28,7 @@ DEVICE_CLASSES = [
|
||||
'gas', # On means gas detected, Off means no gas (clear)
|
||||
'heat', # On means hot, Off means normal
|
||||
'light', # On means light detected, Off means no light
|
||||
'lock', # On means open (unlocked), Off means closed (locked)
|
||||
'moisture', # On means wet, Off means dry
|
||||
'motion', # On means motion detected, Off means no motion (clear)
|
||||
'moving', # On means moving, Off means not moving (stopped)
|
||||
@@ -47,13 +48,12 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
125
homeassistant/components/binary_sensor/bmw_connected_drive.py
Normal file
125
homeassistant/components/binary_sensor/bmw_connected_drive.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Reads vehicle status from BMW connected drive portal.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.bmw_connected_drive/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
||||
|
||||
DEPENDENCIES = ['bmw_connected_drive']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'lids': ['Doors', 'opening'],
|
||||
'windows': ['Windows', 'opening'],
|
||||
'door_lock_state': ['Door lock state', 'safety']
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the BMW sensors."""
|
||||
accounts = hass.data[BMW_DOMAIN]
|
||||
_LOGGER.debug('Found BMW accounts: %s',
|
||||
', '.join([a.name for a in accounts]))
|
||||
devices = []
|
||||
for account in accounts:
|
||||
for vehicle in account.account.vehicles:
|
||||
for key, value in sorted(SENSOR_TYPES.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
"""Representation of a BMW vehicle binary sensor."""
|
||||
|
||||
def __init__(self, account, vehicle, attribute: str, sensor_name,
|
||||
device_class):
|
||||
"""Constructor."""
|
||||
self._account = account
|
||||
self._vehicle = vehicle
|
||||
self._attribute = attribute
|
||||
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
||||
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
|
||||
self._sensor_name = sensor_name
|
||||
self._device_class = device_class
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Data update is triggered from BMWConnectedDriveEntity."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the binary sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the binary sensor."""
|
||||
vehicle_state = self._vehicle.state
|
||||
result = {
|
||||
'car': self._vehicle.name
|
||||
}
|
||||
|
||||
if self._attribute == 'lids':
|
||||
for lid in vehicle_state.lids:
|
||||
result[lid.name] = lid.state.value
|
||||
elif self._attribute == 'windows':
|
||||
for window in vehicle_state.windows:
|
||||
result[window.name] = window.state.value
|
||||
elif self._attribute == 'door_lock_state':
|
||||
result['door_lock_state'] = vehicle_state.door_lock_state.value
|
||||
|
||||
return result
|
||||
|
||||
def update(self):
|
||||
"""Read new state data from the library."""
|
||||
from bimmer_connected.state import LockState
|
||||
vehicle_state = self._vehicle.state
|
||||
|
||||
# device class opening: On means open, Off means closed
|
||||
if self._attribute == 'lids':
|
||||
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
|
||||
self._state = not vehicle_state.all_lids_closed
|
||||
if self._attribute == 'windows':
|
||||
self._state = not vehicle_state.all_windows_closed
|
||||
# device class safety: On means unsafe, Off means safe
|
||||
if self._attribute == 'door_lock_state':
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._state = vehicle_state.door_lock_state not in \
|
||||
[LockState.LOCKED, LockState.SECURED]
|
||||
|
||||
def update_callback(self):
|
||||
"""Schedule a state update."""
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add callback after being added to hass.
|
||||
|
||||
Show latest data after startup.
|
||||
"""
|
||||
self._account.add_update_listener(self.update_callback)
|
||||
@@ -27,7 +27,7 @@ DEFAULT_NAME = 'Alarm'
|
||||
DEFAULT_PORT = '5007'
|
||||
DEFAULT_SSL = False
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=1)
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(DEVICE_CLASSES),
|
||||
|
||||
@@ -4,8 +4,6 @@ Support for deCONZ binary sensor.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import (
|
||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
|
||||
@@ -15,8 +13,8 @@ from homeassistant.core import callback
|
||||
DEPENDENCIES = ['deconz']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -25,8 +23,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
sensors = hass.data[DATA_DECONZ].sensors
|
||||
entities = []
|
||||
|
||||
for key in sorted(sensors.keys(), key=int):
|
||||
sensor = sensors[key]
|
||||
for sensor in sensors.values():
|
||||
if sensor and sensor.type in DECONZ_BINARY_SENSOR:
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
@@ -39,8 +36,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
"""Set up sensor and add update callback to get data from websocket."""
|
||||
self._sensor = sensor
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self._sensor.register_async_callback(self.async_update_callback)
|
||||
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id
|
||||
@@ -96,9 +92,9 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
from pydeconz.sensor import PRESENCE
|
||||
attr = {
|
||||
ATTR_BATTERY_LEVEL: self._sensor.battery,
|
||||
}
|
||||
if self._sensor.type in PRESENCE:
|
||||
attr = {}
|
||||
if self._sensor.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark:
|
||||
attr['dark'] = self._sensor.dark
|
||||
return attr
|
||||
|
||||
78
homeassistant/components/binary_sensor/egardia.py
Normal file
78
homeassistant/components/binary_sensor/egardia.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Interfaces with Egardia/Woonveilig alarm control panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.egardia/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['egardia']
|
||||
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
|
||||
'Door Contact': 'opening',
|
||||
'IR': 'motion'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Initialize the platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
return
|
||||
|
||||
disc_info = discovery_info[ATTR_DISCOVER_DEVICES]
|
||||
# multiple devices here!
|
||||
async_add_devices(
|
||||
(
|
||||
EgardiaBinarySensor(
|
||||
sensor_id=disc_info[sensor]['id'],
|
||||
name=disc_info[sensor]['name'],
|
||||
egardia_system=hass.data[EGARDIA_DEVICE],
|
||||
device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get(
|
||||
disc_info[sensor]['type'], None)
|
||||
)
|
||||
for sensor in disc_info
|
||||
), True)
|
||||
|
||||
|
||||
class EgardiaBinarySensor(BinarySensorDevice):
|
||||
"""Represents a sensor based on an Egardia sensor (IR, Door Contact)."""
|
||||
|
||||
def __init__(self, sensor_id, name, egardia_system, device_class):
|
||||
"""Initialize the sensor device."""
|
||||
self._id = sensor_id
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._device_class = device_class
|
||||
self._egardia_system = egardia_system
|
||||
|
||||
def update(self):
|
||||
"""Update the status."""
|
||||
egardia_input = self._egardia_system.getsensorstate(self._id)
|
||||
self._state = STATE_ON if egardia_input else STATE_OFF
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Whether the device is switched on."""
|
||||
return self._state == STATE_ON
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""Whether the device is hidden by default."""
|
||||
# these type of sensors are probably mainly used for automations
|
||||
return True
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""The device class."""
|
||||
return self._device_class
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.4']
|
||||
REQUIREMENTS = ['pyhik==0.1.8']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
@@ -48,6 +48,9 @@ DEVICE_CLASS_MAP = {
|
||||
'Face Detection': 'motion',
|
||||
'Scene Change Detection': 'motion',
|
||||
'I/O': None,
|
||||
'Unattended Baggage': 'motion',
|
||||
'Attended Baggage': 'motion',
|
||||
'Recording Failure': None,
|
||||
}
|
||||
|
||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||
@@ -211,7 +214,7 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
"""Return a unique ID."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
|
||||
@@ -32,6 +32,7 @@ class HiveBinarySensorEntity(BinarySensorDevice):
|
||||
self.device_type = hivedevice["HA_DeviceType"]
|
||||
self.node_device_type = hivedevice["Hive_DeviceType"]
|
||||
self.session = hivesession
|
||||
self.attributes = {}
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
|
||||
@@ -52,6 +53,11 @@ class HiveBinarySensorEntity(BinarySensorDevice):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self.node_name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Show Device Attributes."""
|
||||
return self.attributes
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
@@ -61,3 +67,5 @@ class HiveBinarySensorEntity(BinarySensorDevice):
|
||||
def update(self):
|
||||
"""Update all Node data from Hive."""
|
||||
self.session.core.update_data(self.node_id)
|
||||
self.attributes = self.session.attributes.state_attributes(
|
||||
self.node_id)
|
||||
|
||||
@@ -2,86 +2,56 @@
|
||||
Support for INSTEON dimmers via PowerLinc Modem.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/insteon_plm/
|
||||
https://home-assistant.io/components/binary_sensor.insteon_plm/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.insteon_plm import InsteonPLMEntity
|
||||
|
||||
DEPENDENCIES = ['insteon_plm']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
'motionSensor': 'motion',
|
||||
'doorSensor': 'door',
|
||||
'wetLeakSensor': 'moisture'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the INSTEON PLM device class for the hass platform."""
|
||||
plm = hass.data['insteon_plm']
|
||||
|
||||
device_list = []
|
||||
for device in discovery_info:
|
||||
name = device.get('address')
|
||||
address = device.get('address_hex')
|
||||
address = discovery_info['address']
|
||||
device = plm.devices[address]
|
||||
state_key = discovery_info['state_key']
|
||||
name = device.states[state_key].name
|
||||
if name != 'dryLeakSensor':
|
||||
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
|
||||
device.address.hex, device.states[state_key].name)
|
||||
|
||||
_LOGGER.info('Registered %s with binary_sensor platform.', name)
|
||||
new_entity = InsteonPLMBinarySensor(device, state_key)
|
||||
|
||||
device_list.append(
|
||||
InsteonPLMBinarySensorDevice(hass, plm, address, name)
|
||||
)
|
||||
|
||||
async_add_devices(device_list)
|
||||
async_add_devices([new_entity])
|
||||
|
||||
|
||||
class InsteonPLMBinarySensorDevice(BinarySensorDevice):
|
||||
"""A Class for an Insteon device."""
|
||||
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
"""A Class for an Insteon device entity."""
|
||||
|
||||
def __init__(self, hass, plm, address, name):
|
||||
"""Initialize the binarysensor."""
|
||||
self._hass = hass
|
||||
self._plm = plm.protocol
|
||||
self._address = address
|
||||
self._name = name
|
||||
|
||||
self._plm.add_update_callback(
|
||||
self.async_binarysensor_update, {'address': self._address})
|
||||
def __init__(self, device, state_key):
|
||||
"""Initialize the INSTEON PLM binary sensor."""
|
||||
super().__init__(device, state_key)
|
||||
self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the address of the node."""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the node."""
|
||||
return self._name
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
sensorstate = self._plm.get_device_attr(self._address, 'sensorstate')
|
||||
_LOGGER.info("Sensor state for %s is %s", self._address, sensorstate)
|
||||
return bool(sensorstate)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Provide attributes for display on device card."""
|
||||
insteon_plm = get_component('insteon_plm')
|
||||
return insteon_plm.common_attributes(self)
|
||||
|
||||
def get_attr(self, key):
|
||||
"""Return specified attribute for this device."""
|
||||
return self._plm.get_device_attr(self.address, key)
|
||||
|
||||
@callback
|
||||
def async_binarysensor_update(self, message):
|
||||
"""Receive notification from transport that new data exists."""
|
||||
_LOGGER.info("Received update callback from PLM for %s", self._address)
|
||||
self._hass.async_add_job(self.async_update_ha_state())
|
||||
return bool(self._insteon_device_state.value)
|
||||
|
||||
@@ -56,24 +56,17 @@ def setup_platform(hass, config: ConfigType,
|
||||
else:
|
||||
device_type = _detect_device_type(node)
|
||||
subnode_id = int(node.nid[-1])
|
||||
if device_type == 'opening':
|
||||
# Door/window sensors use an optional "negative" node
|
||||
if subnode_id == 4:
|
||||
if (device_type == 'opening' or device_type == 'moisture'):
|
||||
# These sensors use an optional "negative" subnode 2 to snag
|
||||
# all state changes
|
||||
if subnode_id == 2:
|
||||
parent_device.add_negative_node(node)
|
||||
elif subnode_id == 4:
|
||||
# Subnode 4 is the heartbeat node, which we will represent
|
||||
# as a separate binary_sensor
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
elif subnode_id == 2:
|
||||
parent_device.add_negative_node(node)
|
||||
elif device_type == 'moisture':
|
||||
# Moisture nodes have a subnode 2, but we ignore it because
|
||||
# it's just the inverse of the primary node.
|
||||
if subnode_id == 4:
|
||||
# Heartbeat node
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
else:
|
||||
# We don't yet have any special logic for other sensor types,
|
||||
# so add the nodes as individual devices
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for KNX/IP binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.knx/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -26,6 +25,7 @@ CONF_DEFAULT_HOOK = 'on'
|
||||
CONF_COUNTER = 'counter'
|
||||
CONF_DEFAULT_COUNTER = 1
|
||||
CONF_ACTION = 'action'
|
||||
CONF_RESET_AFTER = 'reset_after'
|
||||
|
||||
CONF__ACTION = 'turn_off_action'
|
||||
|
||||
@@ -49,12 +49,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
||||
vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_RESET_AFTER): cv.positive_int,
|
||||
vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up binary sensor(s) for KNX platform."""
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
@@ -82,7 +83,8 @@ def async_add_devices_config(hass, config, async_add_devices):
|
||||
name=name,
|
||||
group_address=config.get(CONF_ADDRESS),
|
||||
device_class=config.get(CONF_DEVICE_CLASS),
|
||||
significant_bit=config.get(CONF_SIGNIFICANT_BIT))
|
||||
significant_bit=config.get(CONF_SIGNIFICANT_BIT),
|
||||
reset_after=config.get(CONF_RESET_AFTER))
|
||||
hass.data[DATA_KNX].xknx.devices.add(binary_sensor)
|
||||
|
||||
entity = KNXBinarySensor(hass, binary_sensor)
|
||||
@@ -111,11 +113,10 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
@callback
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/maxcube/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.components.maxcube import DATA_KEY
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -15,16 +15,17 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add window shutters."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
devices = []
|
||||
for handler in hass.data[DATA_KEY].values():
|
||||
cube = handler.cube
|
||||
for device in cube.devices:
|
||||
name = "{} {}".format(
|
||||
cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
for device in cube.devices:
|
||||
name = "{} {}".format(
|
||||
cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
# Only add Window Shutters
|
||||
if cube.is_windowshutter(device):
|
||||
devices.append(MaxCubeShutter(hass, name, device.rf_address))
|
||||
# Only add Window Shutters
|
||||
if cube.is_windowshutter(device):
|
||||
devices.append(
|
||||
MaxCubeShutter(handler, name, device.rf_address))
|
||||
|
||||
if devices:
|
||||
add_devices(devices)
|
||||
@@ -33,12 +34,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class MaxCubeShutter(BinarySensorDevice):
|
||||
"""Representation of a MAX! Cube Binary Sensor device."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
def __init__(self, handler, name, rf_address):
|
||||
"""Initialize MAX! Cube BinarySensorDevice."""
|
||||
self._name = name
|
||||
self._sensor_type = 'window'
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
self._cubehandle = handler
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
"""
|
||||
Support for Mercedes cars with Mercedes ME.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mercedesme/
|
||||
"""
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.mercedesme import (
|
||||
DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS)
|
||||
|
||||
DEPENDENCIES = ['mercedesme']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the sensor platform."""
|
||||
data = hass.data[DATA_MME].data
|
||||
|
||||
if not data.cars:
|
||||
_LOGGER.error("No cars found. Check component log.")
|
||||
return
|
||||
|
||||
devices = []
|
||||
for car in data.cars:
|
||||
for key, value in sorted(BINARY_SENSORS.items()):
|
||||
if car['availabilities'].get(key, 'INVALID') == 'VALID':
|
||||
devices.append(MercedesMEBinarySensor(
|
||||
data, key, value[0], car["vin"], None))
|
||||
else:
|
||||
_LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._internal_name == "windowsClosed":
|
||||
return {
|
||||
"window_front_left": self._car["windowStatusFrontLeft"],
|
||||
"window_front_right": self._car["windowStatusFrontRight"],
|
||||
"window_rear_left": self._car["windowStatusRearLeft"],
|
||||
"window_rear_right": self._car["windowStatusRearRight"],
|
||||
"original_value": self._car[self._internal_name],
|
||||
"last_update": datetime.datetime.fromtimestamp(
|
||||
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"car": self._car["license"]
|
||||
}
|
||||
elif self._internal_name == "tireWarningLight":
|
||||
return {
|
||||
"front_right_tire_pressure_kpa":
|
||||
self._car["frontRightTirePressureKpa"],
|
||||
"front_left_tire_pressure_kpa":
|
||||
self._car["frontLeftTirePressureKpa"],
|
||||
"rear_right_tire_pressure_kpa":
|
||||
self._car["rearRightTirePressureKpa"],
|
||||
"rear_left_tire_pressure_kpa":
|
||||
self._car["rearLeftTirePressureKpa"],
|
||||
"original_value": self._car[self._internal_name],
|
||||
"last_update": datetime.datetime.fromtimestamp(
|
||||
self._car["lastUpdate"]
|
||||
).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"car": self._car["license"],
|
||||
}
|
||||
return {
|
||||
"original_value": self._car[self._internal_name],
|
||||
"last_update": datetime.datetime.fromtimestamp(
|
||||
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"car": self._car["license"]
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
self._car = next(
|
||||
car for car in self._data.cars if car["vin"] == self._vin)
|
||||
|
||||
if self._internal_name == "windowsClosed":
|
||||
self._state = bool(self._car[self._internal_name] == "CLOSED")
|
||||
elif self._internal_name == "tireWarningLight":
|
||||
self._state = bool(self._car[self._internal_name] != "INACTIVE")
|
||||
else:
|
||||
self._state = self._car[self._internal_name] is True
|
||||
|
||||
_LOGGER.debug("Updated %s Value: %s IsOn: %s",
|
||||
self._internal_name, self._state, self.is_on)
|
||||
@@ -9,12 +9,24 @@ from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES, DOMAIN, BinarySensorDevice)
|
||||
from homeassistant.const import STATE_ON
|
||||
|
||||
SENSORS = {
|
||||
'S_DOOR': 'door',
|
||||
'S_MOTION': 'motion',
|
||||
'S_SMOKE': 'smoke',
|
||||
'S_SPRINKLER': 'safety',
|
||||
'S_WATER_LEAK': 'safety',
|
||||
'S_SOUND': 'sound',
|
||||
'S_VIBRATION': 'vibration',
|
||||
'S_MOISTURE': 'moisture',
|
||||
}
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the MySensors platform for binary sensors."""
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the mysensors platform for binary sensors."""
|
||||
mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsBinarySensor,
|
||||
add_devices=add_devices)
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
||||
@@ -29,18 +41,7 @@ class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
pres = self.gateway.const.Presentation
|
||||
class_map = {
|
||||
pres.S_DOOR: 'opening',
|
||||
pres.S_MOTION: 'motion',
|
||||
pres.S_SMOKE: 'smoke',
|
||||
}
|
||||
if float(self.gateway.protocol_version) >= 1.5:
|
||||
class_map.update({
|
||||
pres.S_SPRINKLER: 'sprinkler',
|
||||
pres.S_WATER_LEAK: 'leak',
|
||||
pres.S_SOUND: 'sound',
|
||||
pres.S_VIBRATION: 'vibration',
|
||||
pres.S_MOISTURE: 'moisture',
|
||||
})
|
||||
if class_map.get(self.child_type) in DEVICE_CLASSES:
|
||||
return class_map.get(self.child_type)
|
||||
device_class = SENSORS.get(pres(self.child_type).name)
|
||||
if device_class in DEVICE_CLASSES:
|
||||
return device_class
|
||||
return None
|
||||
|
||||
70
homeassistant/components/binary_sensor/qwikswitch.py
Normal file
70
homeassistant/components/binary_sensor/qwikswitch.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Support for Qwikswitch Binary Sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.qwikswitch/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.qwikswitch import QSEntity, DOMAIN as QWIKSWITCH
|
||||
from homeassistant.core import callback
|
||||
|
||||
DEPENDENCIES = [QWIKSWITCH]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, _, add_devices, discovery_info=None):
|
||||
"""Add binary sensor from the main Qwikswitch component."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
qsusb = hass.data[QWIKSWITCH]
|
||||
_LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s",
|
||||
qsusb, discovery_info)
|
||||
devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]]
|
||||
add_devices(devs)
|
||||
|
||||
|
||||
class QSBinarySensor(QSEntity, BinarySensorDevice):
|
||||
"""Sensor based on a Qwikswitch relay/dimmer module."""
|
||||
|
||||
_val = False
|
||||
|
||||
def __init__(self, sensor):
|
||||
"""Initialize the sensor."""
|
||||
from pyqwikswitch import SENSORS
|
||||
|
||||
super().__init__(sensor['id'], sensor['name'])
|
||||
self.channel = sensor['channel']
|
||||
sensor_type = sensor['type']
|
||||
|
||||
self._decode, _ = SENSORS[sensor_type]
|
||||
self._invert = not sensor.get('invert', False)
|
||||
self._class = sensor.get('class', 'door')
|
||||
|
||||
@callback
|
||||
def update_packet(self, packet):
|
||||
"""Receive update packet from QSUSB."""
|
||||
val = self._decode(packet, channel=self.channel)
|
||||
_LOGGER.debug("Update %s (%s:%s) decoded as %s: %s",
|
||||
self.entity_id, self.qsid, self.channel, val, packet)
|
||||
if val is not None:
|
||||
self._val = bool(val)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Check if device is on (non-zero)."""
|
||||
return self._val == self._invert
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this sensor."""
|
||||
return "qs{}:{}".format(self.qsid, self.channel)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._class
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.14.0']
|
||||
REQUIREMENTS = ['numpy==1.14.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
38
homeassistant/components/binary_sensor/upcloud.py
Normal file
38
homeassistant/components/binary_sensor/upcloud.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Support for monitoring the state of UpCloud servers.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.upcloud/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.upcloud import (
|
||||
UpCloudServerEntity, CONF_SERVERS, DATA_UPCLOUD)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['upcloud']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SERVERS): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the UpCloud server binary sensor."""
|
||||
upcloud = hass.data[DATA_UPCLOUD]
|
||||
|
||||
servers = config.get(CONF_SERVERS)
|
||||
|
||||
devices = [UpCloudBinarySensor(upcloud, uuid) for uuid in servers]
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class UpCloudBinarySensor(UpCloudServerEntity, BinarySensorDevice):
|
||||
"""Representation of an UpCloud server sensor."""
|
||||
@@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.9.3']
|
||||
REQUIREMENTS = ['holidays==0.9.4']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
@@ -30,8 +30,8 @@ ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
|
||||
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
||||
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
|
||||
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
|
||||
'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US',
|
||||
'Wales']
|
||||
'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
|
||||
'UnitedStates', 'US', 'Wales']
|
||||
CONF_COUNTRY = 'country'
|
||||
CONF_PROVINCE = 'province'
|
||||
CONF_WORKDAYS = 'workdays'
|
||||
@@ -47,13 +47,13 @@ DEFAULT_OFFSET = 0
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
|
||||
vol.Optional(CONF_PROVINCE): cv.string,
|
||||
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
|
||||
vol.Optional(CONF_PROVINCE): cv.string,
|
||||
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
})
|
||||
|
||||
|
||||
@@ -74,14 +74,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if province:
|
||||
# 'state' and 'prov' are not interchangeable, so need to make
|
||||
# sure we use the right one
|
||||
if (hasattr(obj_holidays, "PROVINCES") and
|
||||
if (hasattr(obj_holidays, 'PROVINCES') and
|
||||
province in obj_holidays.PROVINCES):
|
||||
obj_holidays = getattr(holidays, country)(prov=province,
|
||||
years=year)
|
||||
elif (hasattr(obj_holidays, "STATES") and
|
||||
obj_holidays = getattr(holidays, country)(
|
||||
prov=province, years=year)
|
||||
elif (hasattr(obj_holidays, 'STATES') and
|
||||
province in obj_holidays.STATES):
|
||||
obj_holidays = getattr(holidays, country)(state=province,
|
||||
years=year)
|
||||
obj_holidays = getattr(holidays, country)(
|
||||
state=province, years=year)
|
||||
else:
|
||||
_LOGGER.error("There is no province/state %s in country %s",
|
||||
province, country)
|
||||
|
||||
@@ -319,7 +319,10 @@ class XiaomiButton(XiaomiBinarySensor):
|
||||
click_type = 'double'
|
||||
elif value == 'both_click':
|
||||
click_type = 'both'
|
||||
elif value == 'shake':
|
||||
click_type = 'shake'
|
||||
else:
|
||||
_LOGGER.warning("Unsupported click_type detected: %s", value)
|
||||
return False
|
||||
|
||||
self._hass.bus.fire('click', {
|
||||
|
||||
@@ -4,7 +4,6 @@ Binary sensors on Zigbee Home Automation networks.
|
||||
For more details on this platform, please refer to the documentation
|
||||
at https://home-assistant.io/components/binary_sensor.zha/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
||||
@@ -25,8 +24,8 @@ CLASS_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Zigbee Home Automation binary sensors."""
|
||||
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||
if discovery_info is None:
|
||||
@@ -39,19 +38,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
device_class = None
|
||||
cluster = in_clusters[IasZone.cluster_id]
|
||||
if discovery_info['new_join']:
|
||||
yield from cluster.bind()
|
||||
await cluster.bind()
|
||||
ieee = cluster.endpoint.device.application.ieee
|
||||
yield from cluster.write_attributes({'cie_addr': ieee})
|
||||
await cluster.write_attributes({'cie_addr': ieee})
|
||||
|
||||
try:
|
||||
zone_type = yield from cluster['zone_type']
|
||||
zone_type = await cluster['zone_type']
|
||||
device_class = CLASS_MAPPING.get(zone_type, None)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# If we fail to read from the device, use a non-specific class
|
||||
pass
|
||||
|
||||
sensor = BinarySensor(device_class, **discovery_info)
|
||||
async_add_devices([sensor])
|
||||
async_add_devices([sensor], update_before_add=True)
|
||||
|
||||
|
||||
class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
@@ -66,6 +65,11 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Let zha handle polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
@@ -83,7 +87,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
if command_id == 0:
|
||||
self._state = args[0] & 3
|
||||
_LOGGER.debug("Updated alarm state: %s", self._state)
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
elif command_id == 1:
|
||||
_LOGGER.debug("Enroll requested")
|
||||
self.hass.add_job(self._ias_zone_cluster.enroll_response(0, 0))
|
||||
res = self._ias_zone_cluster.enroll_response(0, 0)
|
||||
self.hass.async_add_job(res)
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
from bellows.types.basic import uint16_t
|
||||
|
||||
result = await zha.safe_read(self._endpoint.ias_zone,
|
||||
['zone_status'])
|
||||
state = result.get('zone_status', self._state)
|
||||
if isinstance(state, (int, uint16_t)):
|
||||
self._state = result.get('zone_status', self._state) & 3
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
"""
|
||||
Reads vehicle status from BMW connected drive portal.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/bmw_connected_drive/
|
||||
"""
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD
|
||||
)
|
||||
|
||||
REQUIREMENTS = ['bimmer_connected==0.4.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'bmw_connected_drive'
|
||||
CONF_VALUES = 'values'
|
||||
CONF_COUNTRY = 'country'
|
||||
|
||||
ACCOUNT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_COUNTRY): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
cv.string: ACCOUNT_SCHEMA
|
||||
},
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
BMW_COMPONENTS = ['device_tracker', 'sensor']
|
||||
UPDATE_INTERVAL = 5 # in minutes
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the BMW connected drive components."""
|
||||
accounts = []
|
||||
for name, account_config in config[DOMAIN].items():
|
||||
username = account_config[CONF_USERNAME]
|
||||
password = account_config[CONF_PASSWORD]
|
||||
country = account_config[CONF_COUNTRY]
|
||||
_LOGGER.debug('Adding new account %s', name)
|
||||
bimmer = BMWConnectedDriveAccount(username, password, country, name)
|
||||
accounts.append(bimmer)
|
||||
|
||||
# update every UPDATE_INTERVAL minutes, starting now
|
||||
# this should even out the load on the servers
|
||||
|
||||
now = datetime.datetime.now()
|
||||
track_utc_time_change(
|
||||
hass, bimmer.update,
|
||||
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
|
||||
second=now.second)
|
||||
|
||||
hass.data[DOMAIN] = accounts
|
||||
|
||||
for account in accounts:
|
||||
account.update()
|
||||
|
||||
for component in BMW_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BMWConnectedDriveAccount(object):
|
||||
"""Representation of a BMW vehicle."""
|
||||
|
||||
def __init__(self, username: str, password: str, country: str,
|
||||
name: str) -> None:
|
||||
"""Constructor."""
|
||||
from bimmer_connected.account import ConnectedDriveAccount
|
||||
|
||||
self.account = ConnectedDriveAccount(username, password, country)
|
||||
self.name = name
|
||||
self._update_listeners = []
|
||||
|
||||
def update(self, *_):
|
||||
"""Update the state of all vehicles.
|
||||
|
||||
Notify all listeners about the update.
|
||||
"""
|
||||
_LOGGER.debug('Updating vehicle state for account %s, '
|
||||
'notifying %d listeners',
|
||||
self.name, len(self._update_listeners))
|
||||
try:
|
||||
self.account.update_vehicle_states()
|
||||
for listener in self._update_listeners:
|
||||
listener()
|
||||
except IOError as exception:
|
||||
_LOGGER.error('Error updating the vehicle state.')
|
||||
_LOGGER.exception(exception)
|
||||
|
||||
def add_update_listener(self, listener):
|
||||
"""Add a listener for update notifications."""
|
||||
self._update_listeners.append(listener)
|
||||
154
homeassistant/components/bmw_connected_drive/__init__.py
Normal file
154
homeassistant/components/bmw_connected_drive/__init__.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Reads vehicle status from BMW connected drive portal.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/bmw_connected_drive/
|
||||
"""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['bimmer_connected==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'bmw_connected_drive'
|
||||
CONF_REGION = 'region'
|
||||
ATTR_VIN = 'vin'
|
||||
|
||||
ACCOUNT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_REGION): vol.Any('north_america', 'china',
|
||||
'rest_of_world'),
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
cv.string: ACCOUNT_SCHEMA
|
||||
},
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_VIN): cv.string,
|
||||
})
|
||||
|
||||
|
||||
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
|
||||
UPDATE_INTERVAL = 5 # in minutes
|
||||
|
||||
SERVICE_UPDATE_STATE = 'update_state'
|
||||
|
||||
_SERVICE_MAP = {
|
||||
'light_flash': 'trigger_remote_light_flash',
|
||||
'sound_horn': 'trigger_remote_horn',
|
||||
'activate_air_conditioning': 'trigger_remote_air_conditioning',
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config: dict):
|
||||
"""Set up the BMW connected drive components."""
|
||||
accounts = []
|
||||
for name, account_config in config[DOMAIN].items():
|
||||
accounts.append(setup_account(account_config, hass, name))
|
||||
|
||||
hass.data[DOMAIN] = accounts
|
||||
|
||||
def _update_all(call) -> None:
|
||||
"""Update all BMW accounts."""
|
||||
for cd_account in hass.data[DOMAIN]:
|
||||
cd_account.update()
|
||||
|
||||
# Service to manually trigger updates for all accounts.
|
||||
hass.services.register(DOMAIN, SERVICE_UPDATE_STATE, _update_all)
|
||||
|
||||
_update_all(None)
|
||||
|
||||
for component in BMW_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup_account(account_config: dict, hass, name: str) \
|
||||
-> 'BMWConnectedDriveAccount':
|
||||
"""Set up a new BMWConnectedDriveAccount based on the config."""
|
||||
username = account_config[CONF_USERNAME]
|
||||
password = account_config[CONF_PASSWORD]
|
||||
region = account_config[CONF_REGION]
|
||||
_LOGGER.debug('Adding new account %s', name)
|
||||
cd_account = BMWConnectedDriveAccount(username, password, region, name)
|
||||
|
||||
def execute_service(call):
|
||||
"""Execute a service for a vehicle.
|
||||
|
||||
This must be a member function as we need access to the cd_account
|
||||
object here.
|
||||
"""
|
||||
vin = call.data[ATTR_VIN]
|
||||
vehicle = cd_account.account.get_vehicle(vin)
|
||||
if not vehicle:
|
||||
_LOGGER.error('Could not find a vehicle for VIN "%s"!', vin)
|
||||
return
|
||||
function_name = _SERVICE_MAP[call.service]
|
||||
function_call = getattr(vehicle.remote_services, function_name)
|
||||
function_call()
|
||||
|
||||
# register the remote services
|
||||
for service in _SERVICE_MAP:
|
||||
hass.services.register(
|
||||
DOMAIN, service,
|
||||
execute_service,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
# update every UPDATE_INTERVAL minutes, starting now
|
||||
# this should even out the load on the servers
|
||||
now = datetime.datetime.now()
|
||||
track_utc_time_change(
|
||||
hass, cd_account.update,
|
||||
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
|
||||
second=now.second)
|
||||
|
||||
return cd_account
|
||||
|
||||
|
||||
class BMWConnectedDriveAccount(object):
|
||||
"""Representation of a BMW vehicle."""
|
||||
|
||||
def __init__(self, username: str, password: str, region_str: str,
|
||||
name: str) -> None:
|
||||
"""Constructor."""
|
||||
from bimmer_connected.account import ConnectedDriveAccount
|
||||
from bimmer_connected.country_selector import get_region_from_name
|
||||
|
||||
region = get_region_from_name(region_str)
|
||||
|
||||
self.account = ConnectedDriveAccount(username, password, region)
|
||||
self.name = name
|
||||
self._update_listeners = []
|
||||
|
||||
def update(self, *_):
|
||||
"""Update the state of all vehicles.
|
||||
|
||||
Notify all listeners about the update.
|
||||
"""
|
||||
_LOGGER.debug('Updating vehicle state for account %s, '
|
||||
'notifying %d listeners',
|
||||
self.name, len(self._update_listeners))
|
||||
try:
|
||||
self.account.update_vehicle_states()
|
||||
for listener in self._update_listeners:
|
||||
listener()
|
||||
except IOError as exception:
|
||||
_LOGGER.error('Error updating the vehicle state.')
|
||||
_LOGGER.exception(exception)
|
||||
|
||||
def add_update_listener(self, listener):
|
||||
"""Add a listener for update notifications."""
|
||||
self._update_listeners.append(listener)
|
||||
42
homeassistant/components/bmw_connected_drive/services.yaml
Normal file
42
homeassistant/components/bmw_connected_drive/services.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
# Describes the format for available services for bmw_connected_drive
|
||||
#
|
||||
# The services related to locking/unlocking are implemented in the lock
|
||||
# component to avoid redundancy.
|
||||
|
||||
light_flash:
|
||||
description: >
|
||||
Flash the lights of the vehicle. The vehicle is identified via the vin
|
||||
(see below).
|
||||
fields:
|
||||
vin:
|
||||
description: >
|
||||
The vehicle identification number (VIN) of the vehicle, 17 characters
|
||||
example: WBANXXXXXX1234567
|
||||
|
||||
sound_horn:
|
||||
description: >
|
||||
Sound the horn of the vehicle. The vehicle is identified via the vin
|
||||
(see below).
|
||||
fields:
|
||||
vin:
|
||||
description: >
|
||||
The vehicle identification number (VIN) of the vehicle, 17 characters
|
||||
example: WBANXXXXXX1234567
|
||||
|
||||
activate_air_conditioning:
|
||||
description: >
|
||||
Start the air conditioning of the vehicle. What exactly is started here
|
||||
depends on the type of vehicle. It might range from just ventilation over
|
||||
auxilary heating to real air conditioning. The vehicle is identified via
|
||||
the vin (see below).
|
||||
fields:
|
||||
vin:
|
||||
description: >
|
||||
The vehicle identification number (VIN) of the vehicle, 17 characters
|
||||
example: WBANXXXXXX1234567
|
||||
|
||||
update_state:
|
||||
description: >
|
||||
Fetch the last state of the vehicles of all your accounts from the BMW
|
||||
server. This does *not* trigger an update from the vehicle, it just gets
|
||||
the data from the BMW servers. This service does not require any attributes.
|
||||
@@ -194,7 +194,9 @@ class WebDavCalendarData(object):
|
||||
@staticmethod
|
||||
def is_over(vevent):
|
||||
"""Return if the event is over."""
|
||||
return dt.now() > WebDavCalendarData.get_end_date(vevent)
|
||||
return dt.now() >= WebDavCalendarData.to_datetime(
|
||||
WebDavCalendarData.get_end_date(vevent)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_hass_date(obj):
|
||||
@@ -230,4 +232,4 @@ class WebDavCalendarData(object):
|
||||
else:
|
||||
enddate = obj.dtstart.value + timedelta(days=1)
|
||||
|
||||
return WebDavCalendarData.to_datetime(enddate)
|
||||
return enddate
|
||||
|
||||
@@ -11,6 +11,7 @@ from datetime import timedelta
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import (
|
||||
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
|
||||
CONF_IGNORE_AVAILABILITY, CONF_SEARCH,
|
||||
GoogleCalendarService)
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
@@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
||||
'orderBy': 'startTime',
|
||||
'maxResults': 1,
|
||||
'maxResults': 5,
|
||||
'singleEvents': True,
|
||||
}
|
||||
|
||||
@@ -45,24 +46,35 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
def __init__(self, hass, calendar_service, calendar, data):
|
||||
"""Create the Calendar event device."""
|
||||
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||
data.get('search', None))
|
||||
data.get(CONF_SEARCH),
|
||||
data.get(CONF_IGNORE_AVAILABILITY))
|
||||
|
||||
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):
|
||||
def __init__(self, calendar_service, calendar_id, search,
|
||||
ignore_availability):
|
||||
"""Set up how we are going to search the google calendar."""
|
||||
self.calendar_service = calendar_service
|
||||
self.calendar_id = calendar_id
|
||||
self.search = search
|
||||
self.ignore_availability = ignore_availability
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
from httplib2 import ServerNotFoundError
|
||||
|
||||
try:
|
||||
service = self.calendar_service.get()
|
||||
except ServerNotFoundError:
|
||||
_LOGGER.warning("Unable to connect to Google, using cached data")
|
||||
return False
|
||||
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
@@ -73,5 +85,17 @@ class GoogleCalendarData(object):
|
||||
result = events.list(**params).execute()
|
||||
|
||||
items = result.get('items', [])
|
||||
self.event = items[0] if len(items) == 1 else None
|
||||
|
||||
new_event = None
|
||||
for item in items:
|
||||
if (not self.ignore_availability
|
||||
and 'transparency' in item.keys()):
|
||||
if item['transparency'] == 'opaque':
|
||||
new_event = item
|
||||
break
|
||||
else:
|
||||
new_event = item
|
||||
break
|
||||
|
||||
self.event = new_event
|
||||
return True
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
# Describes the format for available calendar services
|
||||
|
||||
todoist:
|
||||
new_task:
|
||||
description: Create a new task and add it to a project.
|
||||
fields:
|
||||
content:
|
||||
description: The name of the task (Required).
|
||||
example: Pick up the mail
|
||||
project:
|
||||
description: The name of the project this task should belong to. Defaults to Inbox (Optional).
|
||||
example: Errands
|
||||
labels:
|
||||
description: Any labels that you want to apply to this task, separated by a comma (Optional).
|
||||
example: Chores,Deliveries
|
||||
priority:
|
||||
description: The priority of this task, from 1 (normal) to 4 (urgent) (Optional).
|
||||
example: 2
|
||||
due_date:
|
||||
description: The day this task is due, in format YYYY-MM-DD (Optional).
|
||||
example: "2018-04-01"
|
||||
todoist_new_task:
|
||||
description: Create a new task and add it to a project.
|
||||
fields:
|
||||
content:
|
||||
description: The name of the task.
|
||||
example: Pick up the mail
|
||||
project:
|
||||
description: The name of the project this task should belong to. Defaults to Inbox.
|
||||
example: Errands
|
||||
labels:
|
||||
description: Any labels that you want to apply to this task, separated by a comma.
|
||||
example: Chores,Deliveries
|
||||
priority:
|
||||
description: The priority of this task, from 1 (normal) to 4 (urgent).
|
||||
example: 2
|
||||
due_date_string:
|
||||
description: The day this task is due, in natural language.
|
||||
example: "tomorrow"
|
||||
due_date_lang:
|
||||
description: The language of due_date_string.
|
||||
example: "en"
|
||||
due_date:
|
||||
description: The day this task is due, in format YYYY-MM-DD.
|
||||
example: "2018-04-01"
|
||||
|
||||
@@ -41,6 +41,14 @@ CONTENT = 'content'
|
||||
DESCRIPTION = 'description'
|
||||
# Calendar Platform: Used in the '_get_date()' method
|
||||
DATETIME = 'dateTime'
|
||||
# Service Call: When is this task due (in natural language)?
|
||||
DUE_DATE_STRING = 'due_date_string'
|
||||
# Service Call: The language of DUE_DATE_STRING
|
||||
DUE_DATE_LANG = 'due_date_lang'
|
||||
# Service Call: The available options of DUE_DATE_LANG
|
||||
DUE_DATE_VALID_LANGS = ['en', 'da', 'pl', 'zh', 'ko', 'de',
|
||||
'pt', 'ja', 'it', 'fr', 'sv', 'ru',
|
||||
'es', 'nl']
|
||||
# Attribute: When is this task due?
|
||||
# Service Call: When is this task due?
|
||||
DUE_DATE = 'due_date'
|
||||
@@ -83,7 +91,11 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower),
|
||||
vol.Optional(LABELS): cv.ensure_list_csv,
|
||||
vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
|
||||
vol.Optional(DUE_DATE): cv.string,
|
||||
|
||||
vol.Exclusive(DUE_DATE_STRING, 'due_date'): cv.string,
|
||||
vol.Optional(DUE_DATE_LANG):
|
||||
vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)),
|
||||
vol.Exclusive(DUE_DATE, 'due_date'): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -186,6 +198,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if PRIORITY in call.data:
|
||||
item.update(priority=call.data[PRIORITY])
|
||||
|
||||
if DUE_DATE_STRING in call.data:
|
||||
item.update(date_string=call.data[DUE_DATE_STRING])
|
||||
|
||||
if DUE_DATE_LANG in call.data:
|
||||
item.update(date_lang=call.data[DUE_DATE_LANG])
|
||||
|
||||
if DUE_DATE in call.data:
|
||||
due_date = dt.parse_datetime(call.data[DUE_DATE])
|
||||
if due_date is None:
|
||||
@@ -496,6 +514,10 @@ class TodoistProjectData(object):
|
||||
# We had no valid tasks
|
||||
return True
|
||||
|
||||
# Make sure the task collection is reset to prevent an
|
||||
# infinite collection repeating the same tasks
|
||||
self.all_project_tasks.clear()
|
||||
|
||||
# Organize the best tasks (so users can see all the tasks
|
||||
# they have, organized)
|
||||
while project_tasks:
|
||||
|
||||
@@ -264,9 +264,9 @@ class Camera(Entity):
|
||||
'boundary=--frameboundary')
|
||||
yield from response.prepare(request)
|
||||
|
||||
def write(img_bytes):
|
||||
async def write(img_bytes):
|
||||
"""Write image to stream."""
|
||||
response.write(bytes(
|
||||
await response.write(bytes(
|
||||
'--frameboundary\r\n'
|
||||
'Content-Type: {}\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
@@ -282,15 +282,14 @@ class Camera(Entity):
|
||||
break
|
||||
|
||||
if img_bytes and img_bytes != last_image:
|
||||
write(img_bytes)
|
||||
yield from write(img_bytes)
|
||||
|
||||
# Chrome seems to always ignore first picture,
|
||||
# print it twice.
|
||||
if last_image is None:
|
||||
write(img_bytes)
|
||||
yield from write(img_bytes)
|
||||
|
||||
last_image = img_bytes
|
||||
yield from response.drain()
|
||||
|
||||
yield from asyncio.sleep(.5)
|
||||
|
||||
|
||||
@@ -49,8 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up an Arlo IP Camera."""
|
||||
arlo = hass.data.get(DATA_ARLO)
|
||||
if not arlo:
|
||||
@@ -60,7 +59,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
for camera in arlo.cameras:
|
||||
cameras.append(ArloCam(hass, camera, config))
|
||||
|
||||
async_add_devices(cameras, True)
|
||||
add_devices(cameras, True)
|
||||
|
||||
|
||||
class ArloCam(Camera):
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.components.camera import (
|
||||
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, 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
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TOPIC = 'topic'
|
||||
|
||||
DEFAULT_NAME = 'MQTT Camera'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
@@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT Camera."""
|
||||
topic = config[CONF_TOPIC]
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
async_add_devices([MqttCamera(config[CONF_NAME], topic)])
|
||||
async_add_devices([MqttCamera(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_TOPIC)
|
||||
)])
|
||||
|
||||
|
||||
class MqttCamera(Camera):
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -33,6 +34,9 @@ DEFAULT_PORT = 5000
|
||||
DEFAULT_USERNAME = 'admin'
|
||||
DEFAULT_PASSWORD = '888888'
|
||||
DEFAULT_ARGUMENTS = '-q:v 2'
|
||||
DEFAULT_PROFILE = 0
|
||||
|
||||
CONF_PROFILE = "profile"
|
||||
|
||||
ATTR_PAN = "pan"
|
||||
ATTR_TILT = "tilt"
|
||||
@@ -57,6 +61,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
})
|
||||
|
||||
SERVICE_PTZ_SCHEMA = vol.Schema({
|
||||
@@ -67,8 +73,7 @@ SERVICE_PTZ_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a ONVIF camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)):
|
||||
return
|
||||
@@ -91,7 +96,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_PTZ, handle_ptz,
|
||||
schema=SERVICE_PTZ_SCHEMA)
|
||||
async_add_devices([ONVIFHassCamera(hass, config)])
|
||||
add_devices([ONVIFHassCamera(hass, config)])
|
||||
|
||||
|
||||
class ONVIFHassCamera(Camera):
|
||||
@@ -99,85 +104,128 @@ class ONVIFHassCamera(Camera):
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a ONVIF camera."""
|
||||
from onvif import ONVIFCamera, exceptions
|
||||
super().__init__()
|
||||
import onvif
|
||||
|
||||
self._username = config.get(CONF_USERNAME)
|
||||
self._password = config.get(CONF_PASSWORD)
|
||||
self._host = config.get(CONF_HOST)
|
||||
self._port = config.get(CONF_PORT)
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
self._profile_index = config.get(CONF_PROFILE)
|
||||
self._input = None
|
||||
camera = None
|
||||
self._media_service = \
|
||||
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
|
||||
self._host, self._port),
|
||||
self._username, self._password,
|
||||
'{}/wsdl/media.wsdl'.format(os.path.dirname(
|
||||
onvif.__file__)))
|
||||
|
||||
self._ptz_service = \
|
||||
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
|
||||
self._host, self._port),
|
||||
self._username, self._password,
|
||||
'{}/wsdl/ptz.wsdl'.format(os.path.dirname(
|
||||
onvif.__file__)))
|
||||
|
||||
def obtain_input_uri(self):
|
||||
"""Set the input uri for the camera."""
|
||||
from onvif import exceptions
|
||||
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
|
||||
self._host, self._port)
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
|
||||
config.get(CONF_HOST), config.get(CONF_PORT))
|
||||
camera = ONVIFCamera(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)
|
||||
)
|
||||
media_service = camera.create_media_service()
|
||||
stream_uri = media_service.GetStreamUri(
|
||||
{'StreamSetup': {'Stream': 'RTP-Unicast', 'Transport': 'RTSP'}}
|
||||
)
|
||||
self._input = stream_uri.Uri.replace(
|
||||
'rtsp://', 'rtsp://{}:{}@'.format(
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD)), 1)
|
||||
profiles = self._media_service.GetProfiles()
|
||||
|
||||
if self._profile_index >= len(profiles):
|
||||
_LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d."
|
||||
" Using the last profile.",
|
||||
self._name, self._profile_index)
|
||||
self._profile_index = -1
|
||||
|
||||
req = self._media_service.create_type('GetStreamUri')
|
||||
|
||||
# pylint: disable=protected-access
|
||||
req.ProfileToken = profiles[self._profile_index]._token
|
||||
uri_no_auth = self._media_service.GetStreamUri(req).Uri
|
||||
uri_for_log = uri_no_auth.replace(
|
||||
'rtsp://', 'rtsp://<user>:<password>@', 1)
|
||||
self._input = uri_no_auth.replace(
|
||||
'rtsp://', 'rtsp://{}:{}@'.format(self._username,
|
||||
self._password), 1)
|
||||
_LOGGER.debug(
|
||||
"ONVIF Camera Using the following URL for %s: %s",
|
||||
self._name, self._input)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Unable to communicate with ONVIF Camera: %s", err)
|
||||
raise
|
||||
try:
|
||||
self._ptz = camera.create_ptz_service()
|
||||
self._name, uri_for_log)
|
||||
# we won't need the media service anymore
|
||||
self._media_service = None
|
||||
except exceptions.ONVIFError as err:
|
||||
self._ptz = None
|
||||
_LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err)
|
||||
_LOGGER.debug("Couldn't setup camera '%s'. Error: %s",
|
||||
self._name, err)
|
||||
return
|
||||
|
||||
def perform_ptz(self, pan, tilt, zoom):
|
||||
"""Perform a PTZ action on the camera."""
|
||||
if self._ptz:
|
||||
from onvif import exceptions
|
||||
if self._ptz_service:
|
||||
pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0
|
||||
tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0
|
||||
zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
|
||||
req = {"Velocity": {
|
||||
"PanTilt": {"_x": pan_val, "_y": tilt_val},
|
||||
"Zoom": {"_x": zoom_val}}}
|
||||
self._ptz.ContinuousMove(req)
|
||||
try:
|
||||
self._ptz_service.ContinuousMove(req)
|
||||
except exceptions.ONVIFError as err:
|
||||
if "Bad Request" in err.reason:
|
||||
self._ptz_service = None
|
||||
_LOGGER.debug("Camera '%s' doesn't support PTZ.",
|
||||
self._name)
|
||||
else:
|
||||
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
if ONVIF_DATA not in self.hass.data:
|
||||
self.hass.data[ONVIF_DATA] = {}
|
||||
self.hass.data[ONVIF_DATA][ENTITIES] = []
|
||||
self.hass.data[ONVIF_DATA][ENTITIES].append(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
|
||||
if not self._input:
|
||||
await self.hass.async_add_job(self.obtain_input_uri)
|
||||
if not self._input:
|
||||
return None
|
||||
|
||||
ffmpeg = ImageFrame(
|
||||
self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
|
||||
|
||||
image = yield from asyncio.shield(ffmpeg.get_image(
|
||||
image = await asyncio.shield(ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
if not self._input:
|
||||
await self.hass.async_add_job(self.obtain_input_uri)
|
||||
if not self._input:
|
||||
return None
|
||||
|
||||
stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary,
|
||||
loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
await stream.open_camera(
|
||||
self._input, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
244
homeassistant/components/camera/proxy.py
Normal file
244
homeassistant/components/camera/proxy.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
Proxy camera platform that enables image processing of camera data.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/proxy
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ENTITY_ID, HTTP_HEADER_HA_AUTH)
|
||||
from homeassistant.components.camera import (
|
||||
PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web)
|
||||
|
||||
REQUIREMENTS = ['pillow==5.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_MAX_IMAGE_WIDTH = "max_image_width"
|
||||
CONF_IMAGE_QUALITY = "image_quality"
|
||||
CONF_IMAGE_REFRESH_RATE = "image_refresh_rate"
|
||||
CONF_FORCE_RESIZE = "force_resize"
|
||||
CONF_MAX_STREAM_WIDTH = "max_stream_width"
|
||||
CONF_STREAM_QUALITY = "stream_quality"
|
||||
CONF_CACHE_IMAGES = "cache_images"
|
||||
|
||||
DEFAULT_BASENAME = "Camera Proxy"
|
||||
DEFAULT_QUALITY = 75
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
|
||||
vol.Optional(CONF_IMAGE_QUALITY): int,
|
||||
vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
|
||||
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
|
||||
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
|
||||
vol.Optional(CONF_MAX_STREAM_WIDTH): int,
|
||||
vol.Optional(CONF_STREAM_QUALITY): int,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Proxy camera platform."""
|
||||
async_add_devices([ProxyCamera(hass, config)])
|
||||
|
||||
|
||||
def _resize_image(image, opts):
|
||||
"""Resize image."""
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
if not opts:
|
||||
return image
|
||||
|
||||
quality = opts.quality or DEFAULT_QUALITY
|
||||
new_width = opts.max_width
|
||||
|
||||
img = Image.open(io.BytesIO(image))
|
||||
imgfmt = str(img.format)
|
||||
if imgfmt != 'PNG' and imgfmt != 'JPEG':
|
||||
_LOGGER.debug("Image is of unsupported type: %s", imgfmt)
|
||||
return image
|
||||
|
||||
(old_width, old_height) = img.size
|
||||
old_size = len(image)
|
||||
if old_width <= new_width:
|
||||
if opts.quality is None:
|
||||
_LOGGER.debug("Image is smaller-than / equal-to requested width")
|
||||
return image
|
||||
new_width = old_width
|
||||
|
||||
scale = new_width / float(old_width)
|
||||
new_height = int((float(old_height)*float(scale)))
|
||||
|
||||
img = img.resize((new_width, new_height), Image.ANTIALIAS)
|
||||
imgbuf = io.BytesIO()
|
||||
img.save(imgbuf, "JPEG", optimize=True, quality=quality)
|
||||
newimage = imgbuf.getvalue()
|
||||
if not opts.force_resize and len(newimage) >= old_size:
|
||||
_LOGGER.debug("Using original image(%d bytes) "
|
||||
"because resized image (%d bytes) is not smaller",
|
||||
old_size, len(newimage))
|
||||
return image
|
||||
|
||||
_LOGGER.debug("Resized image "
|
||||
"from (%dx%d - %d bytes) "
|
||||
"to (%dx%d - %d bytes)",
|
||||
old_width, old_height, old_size,
|
||||
new_width, new_height, len(newimage))
|
||||
return newimage
|
||||
|
||||
|
||||
class ImageOpts():
|
||||
"""The representation of image options."""
|
||||
|
||||
def __init__(self, max_width, quality, force_resize):
|
||||
"""Initialize image options."""
|
||||
self.max_width = max_width
|
||||
self.quality = quality
|
||||
self.force_resize = force_resize
|
||||
|
||||
def __bool__(self):
|
||||
"""Bool evalution rules."""
|
||||
return bool(self.max_width or self.quality)
|
||||
|
||||
|
||||
class ProxyCamera(Camera):
|
||||
"""The representation of a Proxy camera."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a proxy camera component."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._proxied_camera = config.get(CONF_ENTITY_ID)
|
||||
self._name = (
|
||||
config.get(CONF_NAME) or
|
||||
"{} - {}".format(DEFAULT_BASENAME, self._proxied_camera))
|
||||
self._image_opts = ImageOpts(
|
||||
config.get(CONF_MAX_IMAGE_WIDTH),
|
||||
config.get(CONF_IMAGE_QUALITY),
|
||||
config.get(CONF_FORCE_RESIZE))
|
||||
|
||||
self._stream_opts = ImageOpts(
|
||||
config.get(CONF_MAX_STREAM_WIDTH),
|
||||
config.get(CONF_STREAM_QUALITY),
|
||||
True)
|
||||
|
||||
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
|
||||
self._cache_images = bool(
|
||||
config.get(CONF_IMAGE_REFRESH_RATE)
|
||||
or config.get(CONF_CACHE_IMAGES))
|
||||
self._last_image_time = 0
|
||||
self._last_image = None
|
||||
self._headers = (
|
||||
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
|
||||
if self.hass.config.api.api_password is not None
|
||||
else None)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
now = dt_util.utcnow()
|
||||
|
||||
if (self._image_refresh_rate and
|
||||
now < self._last_image_time + self._image_refresh_rate):
|
||||
return self._last_image
|
||||
|
||||
self._last_image_time = now
|
||||
url = "{}/api/camera_proxy/{}".format(
|
||||
self.hass.config.api.base_url, self._proxied_camera)
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = await websession.get(url, headers=self._headers)
|
||||
image = await response.read()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout getting camera image")
|
||||
return self._last_image
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Error getting new camera image: %s", err)
|
||||
return self._last_image
|
||||
|
||||
image = await self.hass.async_add_job(
|
||||
_resize_image, image, self._image_opts)
|
||||
|
||||
if self._cache_images:
|
||||
self._last_image = image
|
||||
return image
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
websession = async_get_clientsession(self.hass)
|
||||
url = "{}/api/camera_proxy_stream/{}".format(
|
||||
self.hass.config.api.base_url, self._proxied_camera)
|
||||
stream_coro = websession.get(url, headers=self._headers)
|
||||
|
||||
if not self._stream_opts:
|
||||
await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
return
|
||||
|
||||
response = aiohttp.web.StreamResponse()
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--frameboundary')
|
||||
await response.prepare(request)
|
||||
|
||||
async def write(img_bytes):
|
||||
"""Write image to stream."""
|
||||
await response.write(bytes(
|
||||
'--frameboundary\r\n'
|
||||
'Content-Type: {}\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
self.content_type, len(img_bytes)),
|
||||
'utf-8') + img_bytes + b'\r\n')
|
||||
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
req = await stream_coro
|
||||
|
||||
try:
|
||||
# This would be nicer as an async generator
|
||||
# But that would only be supported for python >=3.6
|
||||
data = b''
|
||||
stream = req.content
|
||||
while True:
|
||||
chunk = await stream.read(102400)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
jpg_start = data.find(b'\xff\xd8')
|
||||
jpg_end = data.find(b'\xff\xd9')
|
||||
if jpg_start != -1 and jpg_end != -1:
|
||||
image = data[jpg_start:jpg_end + 2]
|
||||
image = await self.hass.async_add_job(
|
||||
_resize_image, image, self._stream_opts)
|
||||
await write(image)
|
||||
data = data[jpg_end + 2:]
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
req.close()
|
||||
response = None
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
await response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
@@ -106,6 +106,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error("'%s' is not a whitelisted directory", file_path)
|
||||
return False
|
||||
|
||||
add_devices([RaspberryCamera(setup_config)])
|
||||
|
||||
|
||||
class RaspberryCamera(Camera):
|
||||
"""Representation of a Raspberry Pi camera."""
|
||||
|
||||
@@ -20,7 +20,7 @@ from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['py-synology==0.1.5']
|
||||
REQUIREMENTS = ['py-synology==0.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Xeoma Cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.xeoma/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -14,7 +13,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyxeoma==1.3']
|
||||
REQUIREMENTS = ['pyxeoma==1.4.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,8 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Discover and setup Xeoma Cameras."""
|
||||
from pyxeoma.xeoma import Xeoma, XeomaError
|
||||
|
||||
@@ -53,8 +52,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
xeoma = Xeoma(host, login, password)
|
||||
|
||||
try:
|
||||
yield from xeoma.async_test_connection()
|
||||
discovered_image_names = yield from xeoma.async_get_image_names()
|
||||
await xeoma.async_test_connection()
|
||||
discovered_image_names = await xeoma.async_get_image_names()
|
||||
discovered_cameras = [
|
||||
{
|
||||
CONF_IMAGE_NAME: image_name,
|
||||
@@ -103,12 +102,11 @@ class XeomaCamera(Camera):
|
||||
self._password = password
|
||||
self._last_image = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from pyxeoma.xeoma import XeomaError
|
||||
try:
|
||||
image = yield from self._xeoma.async_get_camera_image(
|
||||
image = await self._xeoma.async_get_camera_image(
|
||||
self._image, self._username, self._password)
|
||||
self._last_image = image
|
||||
except XeomaError as err:
|
||||
|
||||
@@ -38,8 +38,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass,
|
||||
config,
|
||||
async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up a Yi Camera."""
|
||||
_LOGGER.debug('Received configuration: %s', config)
|
||||
async_add_devices([YiCamera(hass, config)], True)
|
||||
@@ -107,31 +109,29 @@ class YiCamera(Camera):
|
||||
self.user, self.passwd, self.host, self.port, self.path,
|
||||
latest_dir, videos[-1])
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
|
||||
url = yield from self.hass.async_add_job(self.get_latest_video_url)
|
||||
url = await self.hass.async_add_job(self.get_latest_video_url)
|
||||
if url != self._last_url:
|
||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
||||
self._last_image = yield from asyncio.shield(ffmpeg.get_image(
|
||||
self._last_image = await asyncio.shield(ffmpeg.get_image(
|
||||
url, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
||||
self._last_url = url
|
||||
|
||||
return self._last_image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
await stream.open_camera(
|
||||
self._last_url, extra_cmd=self._extra_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
await stream.close()
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['py-canary==0.4.0']
|
||||
REQUIREMENTS = ['py-canary==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ STATE_HEAT = 'heat'
|
||||
STATE_COOL = 'cool'
|
||||
STATE_IDLE = 'idle'
|
||||
STATE_AUTO = 'auto'
|
||||
STATE_MANUAL = 'manual'
|
||||
STATE_DRY = 'dry'
|
||||
STATE_FAN_ONLY = 'fan_only'
|
||||
STATE_ECO = 'eco'
|
||||
@@ -237,14 +238,12 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up climate devices."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
yield from component.async_setup(config)
|
||||
await component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_away_mode_set_service(service):
|
||||
async def async_away_mode_set_service(service):
|
||||
"""Set away mode on target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -253,23 +252,22 @@ def async_setup(hass, config):
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if away_mode:
|
||||
yield from climate.async_turn_away_mode_on()
|
||||
await climate.async_turn_away_mode_on()
|
||||
else:
|
||||
yield from climate.async_turn_away_mode_off()
|
||||
await climate.async_turn_away_mode_off()
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
|
||||
schema=SET_AWAY_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_hold_mode_set_service(service):
|
||||
async def async_hold_mode_set_service(service):
|
||||
"""Set hold mode on target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -277,21 +275,20 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_hold_mode(hold_mode)
|
||||
await climate.async_set_hold_mode(hold_mode)
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
|
||||
schema=SET_HOLD_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_aux_heat_set_service(service):
|
||||
async def async_aux_heat_set_service(service):
|
||||
"""Set auxiliary heater on target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -300,23 +297,22 @@ def async_setup(hass, config):
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if aux_heat:
|
||||
yield from climate.async_turn_aux_heat_on()
|
||||
await climate.async_turn_aux_heat_on()
|
||||
else:
|
||||
yield from climate.async_turn_aux_heat_off()
|
||||
await climate.async_turn_aux_heat_off()
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
|
||||
schema=SET_AUX_HEAT_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_temperature_set_service(service):
|
||||
async def async_temperature_set_service(service):
|
||||
"""Set temperature on the target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -333,21 +329,20 @@ def async_setup(hass, config):
|
||||
else:
|
||||
kwargs[value] = temp
|
||||
|
||||
yield from climate.async_set_temperature(**kwargs)
|
||||
await climate.async_set_temperature(**kwargs)
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
|
||||
schema=SET_TEMPERATURE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_humidity_set_service(service):
|
||||
async def async_humidity_set_service(service):
|
||||
"""Set humidity on the target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -355,20 +350,19 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_humidity(humidity)
|
||||
await climate.async_set_humidity(humidity)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
|
||||
schema=SET_HUMIDITY_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_fan_mode_set_service(service):
|
||||
async def async_fan_mode_set_service(service):
|
||||
"""Set fan mode on target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -376,20 +370,19 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_fan_mode(fan)
|
||||
await climate.async_set_fan_mode(fan)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
|
||||
schema=SET_FAN_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_operation_set_service(service):
|
||||
async def async_operation_set_service(service):
|
||||
"""Set operating mode on the target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -397,20 +390,19 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_operation_mode(operation_mode)
|
||||
await climate.async_set_operation_mode(operation_mode)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
|
||||
schema=SET_OPERATION_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_swing_set_service(service):
|
||||
async def async_swing_set_service(service):
|
||||
"""Set swing mode on the target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -418,36 +410,35 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_swing_mode(swing_mode)
|
||||
await climate.async_set_swing_mode(swing_mode)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
|
||||
schema=SET_SWING_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_on_off_service(service):
|
||||
async def async_on_off_service(service):
|
||||
"""Handle on/off calls."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
yield from climate.async_turn_on()
|
||||
await climate.async_turn_on()
|
||||
elif service.service == SERVICE_TURN_OFF:
|
||||
yield from climate.async_turn_off()
|
||||
await climate.async_turn_off()
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_on_off_service,
|
||||
|
||||
@@ -14,10 +14,10 @@ from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
||||
ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_CONFIGURING = {}
|
||||
@@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
|
||||
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
|
||||
SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -122,6 +122,7 @@ class Thermostat(ClimateDevice):
|
||||
self._climate_list = self.climate_list
|
||||
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
|
||||
'heat', 'off']
|
||||
self._fan_list = ['auto', 'on']
|
||||
self.update_without_throttle = False
|
||||
|
||||
def update(self):
|
||||
@@ -180,24 +181,29 @@ class Thermostat(ClimateDevice):
|
||||
return self.thermostat['runtime']['desiredCool'] / 10.0
|
||||
return None
|
||||
|
||||
@property
|
||||
def desired_fan_mode(self):
|
||||
"""Return the desired fan mode of operation."""
|
||||
return self.thermostat['runtime']['desiredFanMode']
|
||||
|
||||
@property
|
||||
def fan(self):
|
||||
"""Return the current fan state."""
|
||||
"""Return the current fan status."""
|
||||
if 'fan' in self.thermostat['equipmentStatus']:
|
||||
return STATE_ON
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.thermostat['runtime']['desiredFanMode']
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
mode = self._current_hold_mode
|
||||
return None if mode == AWAY_MODE else mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
@property
|
||||
def _current_hold_mode(self):
|
||||
events = self.thermostat['events']
|
||||
@@ -206,7 +212,7 @@ class Thermostat(ClimateDevice):
|
||||
if event['type'] == 'hold':
|
||||
if event['holdClimateRef'] == 'away':
|
||||
if int(event['endDate'][0:4]) - \
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# A temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
# A permanent hold from away climate
|
||||
@@ -228,7 +234,7 @@ class Thermostat(ClimateDevice):
|
||||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
if self.operation_mode == 'auxHeatOnly' or \
|
||||
self.operation_mode == 'heatPump':
|
||||
self.operation_mode == 'heatPump':
|
||||
return STATE_HEAT
|
||||
return self.operation_mode
|
||||
|
||||
@@ -271,10 +277,11 @@ class Thermostat(ClimateDevice):
|
||||
operation = STATE_HEAT
|
||||
else:
|
||||
operation = status
|
||||
|
||||
return {
|
||||
"actual_humidity": self.thermostat['runtime']['actualHumidity'],
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"climate_mode": self.mode,
|
||||
"operation": operation,
|
||||
"climate_list": self.climate_list,
|
||||
"fan_min_on_time": self.fan_min_on_time
|
||||
@@ -342,25 +349,46 @@ class Thermostat(ClimateDevice):
|
||||
cool_temp_setpoint, heat_temp_setpoint,
|
||||
self.hold_preference())
|
||||
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
|
||||
"cool=%s, is=%s", heat_temp, isinstance(
|
||||
heat_temp, (int, float)), cool_temp,
|
||||
"cool=%s, is=%s", heat_temp,
|
||||
isinstance(heat_temp, (int, float)), cool_temp,
|
||||
isinstance(cool_temp, (int, float)))
|
||||
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set the fan mode. Valid values are "on" or "auto"."""
|
||||
if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO):
|
||||
error = "Invalid fan_mode value: Valid values are 'on' or 'auto'"
|
||||
_LOGGER.error(error)
|
||||
return
|
||||
|
||||
cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0
|
||||
heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0
|
||||
self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode,
|
||||
cool_temp, heat_temp,
|
||||
self.hold_preference())
|
||||
|
||||
_LOGGER.info("Setting fan mode to: %s", fan_mode)
|
||||
|
||||
def set_temp_hold(self, temp):
|
||||
"""Set temperature hold in modes other than auto."""
|
||||
# Set arbitrary range when not in auto mode
|
||||
if self.current_operation == STATE_HEAT:
|
||||
"""Set temperature hold in modes other than auto.
|
||||
|
||||
Ecobee API: It is good practice to set the heat and cool hold
|
||||
temperatures to be the same, if the thermostat is in either heat, cool,
|
||||
auxHeatOnly, or off mode. If the thermostat is in auto mode, an
|
||||
additional rule is required. The cool hold temperature must be greater
|
||||
than the heat hold temperature by at least the amount in the
|
||||
heatCoolMinDelta property.
|
||||
https://www.ecobee.com/home/developer/api/examples/ex5.shtml
|
||||
"""
|
||||
if self.current_operation == STATE_HEAT or self.current_operation == \
|
||||
STATE_COOL:
|
||||
heat_temp = temp
|
||||
cool_temp = temp + 20
|
||||
elif self.current_operation == STATE_COOL:
|
||||
heat_temp = temp - 20
|
||||
cool_temp = temp
|
||||
else:
|
||||
# In auto mode set temperature between
|
||||
heat_temp = temp - 10
|
||||
cool_temp = temp + 10
|
||||
delta = self.thermostat['settings']['heatCoolMinDelta'] / 10
|
||||
heat_temp = temp - delta
|
||||
cool_temp = temp + delta
|
||||
self.set_auto_temp_hold(heat_temp, cool_temp)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
@@ -369,8 +397,8 @@ class Thermostat(ClimateDevice):
|
||||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
if self.current_operation == STATE_AUTO and (low_temp is not None or
|
||||
high_temp is not None):
|
||||
if self.current_operation == STATE_AUTO and \
|
||||
(low_temp is not None or high_temp is not None):
|
||||
self.set_auto_temp_hold(low_temp, high_temp)
|
||||
elif temp is not None:
|
||||
self.set_temp_hold(temp)
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.9']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
153
homeassistant/components/climate/fritzbox.py
Executable file
153
homeassistant/components/climate/fritzbox.py
Executable file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
Support for AVM Fritz!Box smarthome thermostate devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
http://home-assistant.io/components/climate.fritzbox/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN
|
||||
from homeassistant.components.fritzbox import (
|
||||
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED)
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS)
|
||||
|
||||
DEPENDENCIES = ['fritzbox']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
|
||||
|
||||
OPERATION_LIST = [STATE_HEAT, STATE_ECO]
|
||||
|
||||
MIN_TEMPERATURE = 8
|
||||
MAX_TEMPERATURE = 28
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Fritzbox smarthome thermostat platform."""
|
||||
devices = []
|
||||
fritz_list = hass.data[FRITZBOX_DOMAIN]
|
||||
|
||||
for fritz in fritz_list:
|
||||
device_list = fritz.get_devices()
|
||||
for device in device_list:
|
||||
if device.has_thermostat:
|
||||
devices.append(FritzboxThermostat(device, fritz))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class FritzboxThermostat(ClimateDevice):
|
||||
"""The thermostat class for Fritzbox smarthome thermostates."""
|
||||
|
||||
def __init__(self, device, fritz):
|
||||
"""Initialize the thermostat."""
|
||||
self._device = device
|
||||
self._fritz = fritz
|
||||
self._current_temperature = self._device.actual_temperature
|
||||
self._target_temperature = self._device.target_temperature
|
||||
self._comfort_temperature = self._device.comfort_temperature
|
||||
self._eco_temperature = self._device.eco_temperature
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if thermostat is available."""
|
||||
return self._device.present
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return precision 0.5."""
|
||||
return PRECISION_HALVES
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if ATTR_OPERATION_MODE in kwargs:
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
self.set_operation_mode(operation_mode)
|
||||
elif ATTR_TEMPERATURE in kwargs:
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self._device.set_target_temperature(temperature)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the current operation mode."""
|
||||
if self._target_temperature == self._comfort_temperature:
|
||||
return STATE_HEAT
|
||||
elif self._target_temperature == self._eco_temperature:
|
||||
return STATE_ECO
|
||||
return STATE_MANUAL
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.set_temperature(temperature=self._comfort_temperature)
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.set_temperature(temperature=self._eco_temperature)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return MIN_TEMPERATURE
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return MAX_TEMPERATURE
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
attrs = {
|
||||
ATTR_STATE_DEVICE_LOCKED: self._device.device_lock,
|
||||
ATTR_STATE_LOCKED: self._device.lock,
|
||||
ATTR_STATE_BATTERY_LOW: self._device.battery_low,
|
||||
}
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
try:
|
||||
self._device.update()
|
||||
self._current_temperature = self._device.actual_temperature
|
||||
self._target_temperature = self._device.target_temperature
|
||||
self._comfort_temperature = self._device.comfort_temperature
|
||||
self._eco_temperature = self._device.eco_temperature
|
||||
except requests.exceptions.HTTPError as ex:
|
||||
_LOGGER.warning("Fritzbox connection error: %s", ex)
|
||||
self._fritz.login()
|
||||
@@ -229,7 +229,7 @@ class GenericThermostat(ClimateDevice):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self._current_operation = STATE_HEAT
|
||||
|
||||
@@ -38,7 +38,10 @@ class HiveClimateEntity(ClimateDevice):
|
||||
self.node_id = hivedevice["Hive_NodeID"]
|
||||
self.node_name = hivedevice["Hive_NodeName"]
|
||||
self.device_type = hivedevice["HA_DeviceType"]
|
||||
if self.device_type == "Heating":
|
||||
self.thermostat_node_id = hivedevice["Thermostat_NodeID"]
|
||||
self.session = hivesession
|
||||
self.attributes = {}
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
|
||||
@@ -71,6 +74,11 @@ class HiveClimateEntity(ClimateDevice):
|
||||
friendly_name = "Hot Water"
|
||||
return friendly_name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Show Device Attributes."""
|
||||
return self.attributes
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
@@ -175,4 +183,9 @@ class HiveClimateEntity(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update all Node data from Hive."""
|
||||
node = self.node_id
|
||||
if self.device_type == "Heating":
|
||||
node = self.thermostat_node_id
|
||||
|
||||
self.session.core.update_data(self.node_id)
|
||||
self.attributes = self.session.attributes.state_attributes(node)
|
||||
|
||||
@@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE, CONF_REGION)
|
||||
|
||||
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.0']
|
||||
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for KNX/IP climate devices.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.knx/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -61,8 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up climate(s) for KNX platform."""
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
@@ -135,11 +134,10 @@ class KNXClimate(ClimateDevice):
|
||||
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
@@ -187,14 +185,13 @@ class KNXClimate(ClimateDevice):
|
||||
"""Return the maximum temperature."""
|
||||
return self.device.target_temperature_max
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
yield from self.device.set_target_temperature(temperature)
|
||||
yield from self.async_update_ha_state()
|
||||
await self.device.set_target_temperature(temperature)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
@@ -210,10 +207,9 @@ class KNXClimate(ClimateDevice):
|
||||
operation_mode in
|
||||
self.device.get_supported_operation_modes()]
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_operation_mode(self, operation_mode):
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if self.device.supports_operation_mode:
|
||||
from xknx.knx import HVACOperationMode
|
||||
knx_operation_mode = HVACOperationMode(operation_mode)
|
||||
yield from self.device.set_operation_mode(knx_operation_mode)
|
||||
await self.device.set_operation_mode(knx_operation_mode)
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.components.maxcube import DATA_KEY
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -24,16 +24,16 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add thermostats."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
|
||||
devices = []
|
||||
for handler in hass.data[DATA_KEY].values():
|
||||
cube = handler.cube
|
||||
for device in cube.devices:
|
||||
name = '{} {}'.format(
|
||||
cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
for device in cube.devices:
|
||||
name = '{} {}'.format(
|
||||
cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
|
||||
devices.append(MaxCubeClimate(hass, name, device.rf_address))
|
||||
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
|
||||
devices.append(
|
||||
MaxCubeClimate(handler, name, device.rf_address))
|
||||
|
||||
if devices:
|
||||
add_devices(devices)
|
||||
@@ -42,14 +42,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class MaxCubeClimate(ClimateDevice):
|
||||
"""MAX! Cube ClimateDevice."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
def __init__(self, handler, name, rf_address):
|
||||
"""Initialize MAX! Cube ClimateDevice."""
|
||||
self._name = name
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
|
||||
STATE_VACATION]
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
self._cubehandle = handler
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
||||
148
homeassistant/components/climate/modbus.py
Normal file
148
homeassistant/components/climate/modbus.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
Platform for a Generic Modbus Thermostat.
|
||||
|
||||
This uses a setpoint and process
|
||||
value within the controller, so both the current temperature register and the
|
||||
target temperature register need to be configured.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.modbus/
|
||||
"""
|
||||
import logging
|
||||
import struct
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE)
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
|
||||
|
||||
import homeassistant.components.modbus as modbus
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['modbus']
|
||||
|
||||
# Parameters not defined by homeassistant.const
|
||||
CONF_TARGET_TEMP = 'target_temp_register'
|
||||
CONF_CURRENT_TEMP = 'current_temp_register'
|
||||
CONF_DATA_TYPE = 'data_type'
|
||||
CONF_COUNT = 'data_count'
|
||||
CONF_PRECISION = 'precision'
|
||||
|
||||
DATA_TYPE_INT = 'int'
|
||||
DATA_TYPE_UINT = 'uint'
|
||||
DATA_TYPE_FLOAT = 'float'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_SLAVE): cv.positive_int,
|
||||
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
|
||||
vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
|
||||
vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT):
|
||||
vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]),
|
||||
vol.Optional(CONF_COUNT, default=2): cv.positive_int,
|
||||
vol.Optional(CONF_PRECISION, default=1): cv.positive_int
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Modbus Thermostat Platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
modbus_slave = config.get(CONF_SLAVE)
|
||||
target_temp_register = config.get(CONF_TARGET_TEMP)
|
||||
current_temp_register = config.get(CONF_CURRENT_TEMP)
|
||||
data_type = config.get(CONF_DATA_TYPE)
|
||||
count = config.get(CONF_COUNT)
|
||||
precision = config.get(CONF_PRECISION)
|
||||
|
||||
add_devices([ModbusThermostat(name, modbus_slave,
|
||||
target_temp_register, current_temp_register,
|
||||
data_type, count, precision)], True)
|
||||
|
||||
|
||||
class ModbusThermostat(ClimateDevice):
|
||||
"""Representation of a Modbus Thermostat."""
|
||||
|
||||
def __init__(self, name, modbus_slave, target_temp_register,
|
||||
current_temp_register, data_type, count, precision):
|
||||
"""Initialize the unit."""
|
||||
self._name = name
|
||||
self._slave = modbus_slave
|
||||
self._target_temperature_register = target_temp_register
|
||||
self._current_temperature_register = current_temp_register
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._data_type = data_type
|
||||
self._count = int(count)
|
||||
self._precision = precision
|
||||
self._structure = '>f'
|
||||
|
||||
data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'},
|
||||
DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'},
|
||||
DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}}
|
||||
|
||||
self._structure = '>{}'.format(data_types[self._data_type]
|
||||
[self._count])
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
def update(self):
|
||||
"""Update Target & Current Temperature."""
|
||||
self._target_temperature = self.read_register(
|
||||
self._target_temperature_register)
|
||||
self._current_temperature = self.read_register(
|
||||
self._current_temperature_register)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the climate device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if target_temperature is None:
|
||||
return
|
||||
byte_string = struct.pack(self._structure, target_temperature)
|
||||
register_value = struct.unpack('>h', byte_string[0:2])[0]
|
||||
|
||||
try:
|
||||
self.write_register(self._target_temperature_register,
|
||||
register_value)
|
||||
except AttributeError as ex:
|
||||
_LOGGER.error(ex)
|
||||
|
||||
def read_register(self, register):
|
||||
"""Read holding register using the modbus hub slave."""
|
||||
try:
|
||||
result = modbus.HUB.read_holding_registers(self._slave, register,
|
||||
self._count)
|
||||
except AttributeError as ex:
|
||||
_LOGGER.error(ex)
|
||||
byte_string = b''.join(
|
||||
[x.to_bytes(2, byteorder='big') for x in result.registers])
|
||||
val = struct.unpack(self._structure, byte_string)[0]
|
||||
register_value = format(val, '.{}f'.format(self._precision))
|
||||
return register_value
|
||||
|
||||
def write_register(self, register, value):
|
||||
"""Write register using the modbus hub slave."""
|
||||
modbus.HUB.write_registers(self._slave, register, [value, 0])
|
||||
@@ -31,10 +31,12 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the MySensors climate."""
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the mysensors climate."""
|
||||
mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices)
|
||||
hass, DOMAIN, discovery_info, MySensorsHVAC,
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
@@ -163,8 +165,8 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
self._values[self.value_type] = operation_mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
super().update()
|
||||
await super().async_update()
|
||||
self._values[self.value_type] = DICT_MYS_TO_HA[
|
||||
self._values[self.value_type]]
|
||||
|
||||
@@ -29,10 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
NEST_MODE_HEAT_COOL = 'heat-cool'
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Nest thermostat."""
|
||||
@@ -58,6 +54,10 @@ class NestThermostat(ClimateDevice):
|
||||
self.device = device
|
||||
self._fan_list = [STATE_ON, STATE_AUTO]
|
||||
|
||||
# Set the default supported features
|
||||
self._support_flags = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE)
|
||||
|
||||
# Not all nest devices support cooling and heating remove unused
|
||||
self._operation_list = [STATE_OFF]
|
||||
|
||||
@@ -70,11 +70,16 @@ class NestThermostat(ClimateDevice):
|
||||
|
||||
if self.device.can_heat and self.device.can_cool:
|
||||
self._operation_list.append(STATE_AUTO)
|
||||
self._support_flags = (self._support_flags |
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
|
||||
self._operation_list.append(STATE_ECO)
|
||||
|
||||
# feature of device
|
||||
self._has_fan = self.device.has_fan
|
||||
if self._has_fan:
|
||||
self._support_flags = (self._support_flags | SUPPORT_FAN_MODE)
|
||||
|
||||
# data attributes
|
||||
self._away = None
|
||||
@@ -95,7 +100,7 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -162,6 +167,7 @@ class NestThermostat(ClimateDevice):
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
import nest
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
@@ -170,7 +176,10 @@ class NestThermostat(ClimateDevice):
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
self.device.target = temp
|
||||
try:
|
||||
self.device.target = temp
|
||||
except nest.nest.APIError:
|
||||
_LOGGER.error("An error occurred while setting the temperature")
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
@@ -178,6 +187,11 @@ class NestThermostat(ClimateDevice):
|
||||
device_mode = operation_mode
|
||||
elif operation_mode == STATE_AUTO:
|
||||
device_mode = NEST_MODE_HEAT_COOL
|
||||
else:
|
||||
device_mode = STATE_OFF
|
||||
_LOGGER.error(
|
||||
"An error occurred while setting device mode. "
|
||||
"Invalid operation mode: %s", operation_mode)
|
||||
self.device.mode = device_mode
|
||||
|
||||
@property
|
||||
@@ -205,11 +219,14 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return self._fan_list
|
||||
if self._has_fan:
|
||||
return self._fan_list
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Turn fan on/off."""
|
||||
self.device.fan = fan_mode.lower()
|
||||
if self._has_fan:
|
||||
self.device.fan = fan_mode.lower()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
||||
@@ -240,13 +240,18 @@ class SensiboClimate(ClimateDevice):
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._temperatures_list[0] \
|
||||
if self._temperatures_list else super().min_temp()
|
||||
if self._temperatures_list else super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._temperatures_list[-1] \
|
||||
if self._temperatures_list else super().max_temp()
|
||||
if self._temperatures_list else super().max_temp
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID based on Sensibo ID."""
|
||||
return self._id
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
|
||||
@@ -15,12 +15,12 @@ import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE)
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME)
|
||||
from homeassistant.helpers import entityfilter, config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||
from homeassistant.components.google_assistant import smart_home as ga_sh
|
||||
from homeassistant.components.google_assistant import helpers as ga_h
|
||||
|
||||
from . import http_api, iot
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
@@ -37,6 +37,7 @@ CONF_FILTER = 'filter'
|
||||
CONF_GOOGLE_ACTIONS = 'google_actions'
|
||||
CONF_RELAYER = 'relayer'
|
||||
CONF_USER_POOL_ID = 'user_pool_id'
|
||||
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
|
||||
|
||||
DEFAULT_MODE = 'production'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -51,7 +52,6 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
|
||||
GOOGLE_ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_TYPE): vol.In(ga_sh.MAPPING_COMPONENT),
|
||||
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
|
||||
})
|
||||
|
||||
@@ -76,6 +76,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_USER_POOL_ID): str,
|
||||
vol.Optional(CONF_REGION): str,
|
||||
vol.Optional(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
}),
|
||||
@@ -111,7 +112,7 @@ class Cloud:
|
||||
|
||||
def __init__(self, hass, mode, alexa, google_actions,
|
||||
cognito_client_id=None, user_pool_id=None, region=None,
|
||||
relayer=None):
|
||||
relayer=None, google_actions_sync_url=None):
|
||||
"""Create an instance of Cloud."""
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
@@ -129,6 +130,7 @@ class Cloud:
|
||||
self.user_pool_id = user_pool_id
|
||||
self.region = region
|
||||
self.relayer = relayer
|
||||
self.google_actions_sync_url = google_actions_sync_url
|
||||
|
||||
else:
|
||||
info = SERVERS[mode]
|
||||
@@ -137,6 +139,7 @@ class Cloud:
|
||||
self.user_pool_id = info['user_pool_id']
|
||||
self.region = info['region']
|
||||
self.relayer = info['relayer']
|
||||
self.google_actions_sync_url = info['google_actions_sync_url']
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
@@ -175,7 +178,7 @@ class Cloud:
|
||||
"""If an entity should be exposed."""
|
||||
return conf['filter'](entity.entity_id)
|
||||
|
||||
self._gactions_config = ga_sh.Config(
|
||||
self._gactions_config = ga_h.Config(
|
||||
should_expose=should_expose,
|
||||
agent_user_id=self.claims['cognito:username'],
|
||||
entity_config=conf.get(CONF_ENTITY_CONFIG),
|
||||
|
||||
@@ -17,14 +17,6 @@ class UserNotConfirmed(CloudError):
|
||||
"""Raised when a user has not confirmed email yet."""
|
||||
|
||||
|
||||
class ExpiredCode(CloudError):
|
||||
"""Raised when an expired code is encountered."""
|
||||
|
||||
|
||||
class InvalidCode(CloudError):
|
||||
"""Raised when an invalid code is submitted."""
|
||||
|
||||
|
||||
class PasswordChangeRequired(CloudError):
|
||||
"""Raised when a password change is required."""
|
||||
|
||||
@@ -42,10 +34,8 @@ class UnknownError(CloudError):
|
||||
AWS_EXCEPTIONS = {
|
||||
'UserNotFoundException': UserNotFound,
|
||||
'NotAuthorizedException': Unauthenticated,
|
||||
'ExpiredCodeException': ExpiredCode,
|
||||
'UserNotConfirmedException': UserNotConfirmed,
|
||||
'PasswordResetRequiredException': PasswordChangeRequired,
|
||||
'CodeMismatchException': InvalidCode,
|
||||
}
|
||||
|
||||
|
||||
@@ -69,17 +59,6 @@ def register(cloud, email, password):
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def confirm_register(cloud, confirmation_code, email):
|
||||
"""Confirm confirmation code after registration."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(cloud)
|
||||
try:
|
||||
cognito.confirm_sign_up(confirmation_code, email)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def resend_email_confirm(cloud, email):
|
||||
"""Resend email confirmation."""
|
||||
from botocore.exceptions import ClientError
|
||||
@@ -107,18 +86,6 @@ def forgot_password(cloud, email):
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def confirm_forgot_password(cloud, confirmation_code, email, new_password):
|
||||
"""Confirm forgotten password code and change password."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(cloud, username=email)
|
||||
|
||||
try:
|
||||
cognito.confirm_forgot_password(confirmation_code, new_password)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def login(cloud, email, password):
|
||||
"""Log user in and fetch certificate."""
|
||||
cognito = _authenticate(cloud, email, password)
|
||||
|
||||
@@ -8,7 +8,9 @@ SERVERS = {
|
||||
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
|
||||
'user_pool_id': 'us-east-1_87ll5WOP8',
|
||||
'region': 'us-east-1',
|
||||
'relayer': 'wss://cloud.hass.io:8000/websocket'
|
||||
'relayer': 'wss://cloud.hass.io:8000/websocket',
|
||||
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
|
||||
'amazonaws.com/prod/smart_home_sync'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,17 +16,15 @@ from .const import DOMAIN, REQUEST_TIMEOUT
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
async def async_setup(hass):
|
||||
"""Initialize the HTTP API."""
|
||||
hass.http.register_view(GoogleActionsSyncView)
|
||||
hass.http.register_view(CloudLoginView)
|
||||
hass.http.register_view(CloudLogoutView)
|
||||
hass.http.register_view(CloudAccountView)
|
||||
hass.http.register_view(CloudRegisterView)
|
||||
hass.http.register_view(CloudConfirmRegisterView)
|
||||
hass.http.register_view(CloudResendConfirmView)
|
||||
hass.http.register_view(CloudForgotPasswordView)
|
||||
hass.http.register_view(CloudConfirmForgotPasswordView)
|
||||
|
||||
|
||||
_CLOUD_ERRORS = {
|
||||
@@ -34,20 +32,17 @@ _CLOUD_ERRORS = {
|
||||
auth_api.UserNotConfirmed: (400, 'Email not confirmed.'),
|
||||
auth_api.Unauthenticated: (401, 'Authentication failed.'),
|
||||
auth_api.PasswordChangeRequired: (400, 'Password change required.'),
|
||||
auth_api.ExpiredCode: (400, 'Confirmation code has expired.'),
|
||||
auth_api.InvalidCode: (400, 'Invalid confirmation code.'),
|
||||
asyncio.TimeoutError: (502, 'Unable to reach the Home Assistant cloud.')
|
||||
}
|
||||
|
||||
|
||||
def _handle_cloud_errors(handler):
|
||||
"""Handle auth errors."""
|
||||
@asyncio.coroutine
|
||||
@wraps(handler)
|
||||
def error_handler(view, request, *args, **kwargs):
|
||||
async def error_handler(view, request, *args, **kwargs):
|
||||
"""Handle exceptions that raise from the wrapped request handler."""
|
||||
try:
|
||||
result = yield from handler(view, request, *args, **kwargs)
|
||||
result = await handler(view, request, *args, **kwargs)
|
||||
return result
|
||||
|
||||
except (auth_api.CloudError, asyncio.TimeoutError) as err:
|
||||
@@ -61,6 +56,31 @@ def _handle_cloud_errors(handler):
|
||||
return error_handler
|
||||
|
||||
|
||||
class GoogleActionsSyncView(HomeAssistantView):
|
||||
"""Trigger a Google Actions Smart Home Sync."""
|
||||
|
||||
url = '/api/cloud/google_actions/sync'
|
||||
name = 'api:cloud:google_actions/sync'
|
||||
|
||||
@_handle_cloud_errors
|
||||
async def post(self, request):
|
||||
"""Trigger a Google Actions sync."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
websession = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
await hass.async_add_job(auth_api.check_token, cloud)
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
req = await websession.post(
|
||||
cloud.google_actions_sync_url, headers={
|
||||
'authorization': cloud.id_token
|
||||
})
|
||||
|
||||
return self.json({}, status_code=req.status)
|
||||
|
||||
|
||||
class CloudLoginView(HomeAssistantView):
|
||||
"""Login to Home Assistant cloud."""
|
||||
|
||||
@@ -72,19 +92,18 @@ class CloudLoginView(HomeAssistantView):
|
||||
vol.Required('email'): str,
|
||||
vol.Required('password'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle login request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
|
||||
data['password'])
|
||||
await hass.async_add_job(auth_api.login, cloud, data['email'],
|
||||
data['password'])
|
||||
|
||||
hass.async_add_job(cloud.iot.connect)
|
||||
# Allow cloud to start connecting.
|
||||
yield from asyncio.sleep(0, loop=hass.loop)
|
||||
await asyncio.sleep(0, loop=hass.loop)
|
||||
return self.json(_account_data(cloud))
|
||||
|
||||
|
||||
@@ -95,14 +114,13 @@ class CloudLogoutView(HomeAssistantView):
|
||||
name = 'api:cloud:logout'
|
||||
|
||||
@_handle_cloud_errors
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
"""Handle logout request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from cloud.logout()
|
||||
await cloud.logout()
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
@@ -113,8 +131,7 @@ class CloudAccountView(HomeAssistantView):
|
||||
url = '/api/cloud/account'
|
||||
name = 'api:cloud:account'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
"""Get account info."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
@@ -136,44 +153,18 @@ class CloudRegisterView(HomeAssistantView):
|
||||
vol.Required('email'): str,
|
||||
vol.Required('password'): vol.All(str, vol.Length(min=6)),
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle registration request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
await hass.async_add_job(
|
||||
auth_api.register, cloud, data['email'], data['password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudConfirmRegisterView(HomeAssistantView):
|
||||
"""Confirm registration on the Home Assistant cloud."""
|
||||
|
||||
url = '/api/cloud/confirm_register'
|
||||
name = 'api:cloud:confirm_register'
|
||||
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('confirmation_code'): str,
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle registration confirmation request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_register, cloud, data['confirmation_code'],
|
||||
data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudResendConfirmView(HomeAssistantView):
|
||||
"""Resend email confirmation code."""
|
||||
|
||||
@@ -184,14 +175,13 @@ class CloudResendConfirmView(HomeAssistantView):
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle resending confirm email code request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
await hass.async_add_job(
|
||||
auth_api.resend_email_confirm, cloud, data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
@@ -207,46 +197,18 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle forgot password request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
await hass.async_add_job(
|
||||
auth_api.forgot_password, cloud, data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudConfirmForgotPasswordView(HomeAssistantView):
|
||||
"""View to finish Forgot Password flow.."""
|
||||
|
||||
url = '/api/cloud/confirm_forgot_password'
|
||||
name = 'api:cloud:confirm_forgot_password'
|
||||
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('confirmation_code'): str,
|
||||
vol.Required('email'): str,
|
||||
vol.Required('new_password'): vol.All(str, vol.Length(min=6))
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password confirm request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_forgot_password, cloud,
|
||||
data['confirmation_code'], data['email'],
|
||||
data['new_password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
def _account_data(cloud):
|
||||
"""Generate the auth data JSON response."""
|
||||
claims = cloud.claims
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Module to handle messages from Home Assistant cloud."""
|
||||
import asyncio
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
from aiohttp import hdrs, client_exceptions, WSMsgType
|
||||
|
||||
@@ -154,7 +155,9 @@ class CloudIoT:
|
||||
disconnect_warn = 'Received invalid JSON.'
|
||||
break
|
||||
|
||||
_LOGGER.debug("Received message: %s", msg)
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_LOGGER.debug("Received message:\n%s\n",
|
||||
pprint.pformat(msg))
|
||||
|
||||
response = {
|
||||
'msgid': msg['msgid'],
|
||||
@@ -176,7 +179,9 @@ class CloudIoT:
|
||||
_LOGGER.exception("Error handling message")
|
||||
response['error'] = 'exception'
|
||||
|
||||
_LOGGER.debug("Publishing message: %s", response)
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_LOGGER.debug("Publishing message:\n%s\n",
|
||||
pprint.pformat(response))
|
||||
yield from client.send_json(response)
|
||||
|
||||
except client_exceptions.WSServerHandshakeError as err:
|
||||
|
||||
@@ -14,9 +14,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/balloob/coinbase-python/archive/'
|
||||
'3a35efe13ef728a1cc18204b4f25be1fcb1c6006.zip#coinbase==2.0.8a1']
|
||||
REQUIREMENTS = ['coinbase==2.1.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -13,49 +13,31 @@ from homeassistant.util.yaml import load_yaml, dump
|
||||
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script')
|
||||
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
|
||||
'entity_registry', 'config_entries')
|
||||
ON_DEMAND = ('zwave',)
|
||||
FEATURE_FLAGS = ('config_entries',)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the config component."""
|
||||
global SECTIONS
|
||||
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
'config', 'config', 'mdi:settings')
|
||||
|
||||
# Temporary way of allowing people to opt-in for unreleased config sections
|
||||
for key, value in config.get(DOMAIN, {}).items():
|
||||
if key in FEATURE_FLAGS and value:
|
||||
SECTIONS += (key,)
|
||||
|
||||
@asyncio.coroutine
|
||||
def setup_panel(panel_name):
|
||||
async def setup_panel(panel_name):
|
||||
"""Set up a panel."""
|
||||
panel = yield from async_prepare_setup_platform(
|
||||
panel = await async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, panel_name)
|
||||
|
||||
if not panel:
|
||||
return
|
||||
|
||||
success = yield from panel.async_setup(hass)
|
||||
success = await panel.async_setup(hass)
|
||||
|
||||
if success:
|
||||
key = '{}.{}'.format(DOMAIN, panel_name)
|
||||
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key})
|
||||
hass.config.components.add(key)
|
||||
|
||||
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
|
||||
|
||||
for panel_name in ON_DEMAND:
|
||||
if panel_name in hass.config.components:
|
||||
tasks.append(setup_panel(panel_name))
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@callback
|
||||
def component_loaded(event):
|
||||
"""Respond to components being loaded."""
|
||||
@@ -65,6 +47,15 @@ def async_setup(hass, config):
|
||||
|
||||
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
|
||||
|
||||
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
|
||||
|
||||
for panel_name in ON_DEMAND:
|
||||
if panel_name in hass.config.components:
|
||||
tasks.append(setup_panel(panel_name))
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -93,11 +84,10 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
"""Set value."""
|
||||
raise NotImplementedError
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, config_key):
|
||||
async def get(self, request, config_key):
|
||||
"""Fetch device specific config."""
|
||||
hass = request.app['hass']
|
||||
current = yield from self.read_config(hass)
|
||||
current = await self.read_config(hass)
|
||||
value = self._get_value(hass, current, config_key)
|
||||
|
||||
if value is None:
|
||||
@@ -105,11 +95,10 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
|
||||
return self.json(value)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, config_key):
|
||||
async def post(self, request, config_key):
|
||||
"""Validate config and return results."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
data = await request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified', 400)
|
||||
|
||||
@@ -128,10 +117,10 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
hass = request.app['hass']
|
||||
path = hass.config.path(self.path)
|
||||
|
||||
current = yield from self.read_config(hass)
|
||||
current = await self.read_config(hass)
|
||||
self._write_value(hass, current, config_key, data)
|
||||
|
||||
yield from hass.async_add_job(_write, path, current)
|
||||
await hass.async_add_job(_write, path, current)
|
||||
|
||||
if self.post_write_hook is not None:
|
||||
hass.async_add_job(self.post_write_hook(hass))
|
||||
@@ -140,10 +129,9 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
'result': 'ok',
|
||||
})
|
||||
|
||||
@asyncio.coroutine
|
||||
def read_config(self, hass):
|
||||
async def read_config(self, hass):
|
||||
"""Read the config."""
|
||||
current = yield from hass.async_add_job(
|
||||
current = await hass.async_add_job(
|
||||
_read, hass.config.path(self.path))
|
||||
if not current:
|
||||
current = self._empty_config()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Provide configuration end points for Automations."""
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.components.config import EditIdBasedConfigView
|
||||
from homeassistant.components.automation import (
|
||||
PLATFORM_SCHEMA, DOMAIN, async_reload)
|
||||
@@ -13,8 +15,38 @@ CONFIG_PATH = 'automations.yaml'
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Set up the Automation config API."""
|
||||
hass.http.register_view(EditIdBasedConfigView(
|
||||
hass.http.register_view(EditAutomationConfigView(
|
||||
DOMAIN, 'config', CONFIG_PATH, cv.string,
|
||||
PLATFORM_SCHEMA, post_write_hook=async_reload
|
||||
))
|
||||
return True
|
||||
|
||||
|
||||
class EditAutomationConfigView(EditIdBasedConfigView):
|
||||
"""Edit automation config."""
|
||||
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
index = None
|
||||
for index, cur_value in enumerate(data):
|
||||
if cur_value[CONF_ID] == config_key:
|
||||
break
|
||||
else:
|
||||
cur_value = OrderedDict()
|
||||
cur_value[CONF_ID] = config_key
|
||||
index = len(data)
|
||||
data.append(cur_value)
|
||||
|
||||
# Iterate through some keys that we want to have ordered in the output
|
||||
updated_value = OrderedDict()
|
||||
for key in ('id', 'alias', 'trigger', 'condition', 'action'):
|
||||
if key in cur_value:
|
||||
updated_value[key] = cur_value[key]
|
||||
if key in new_value:
|
||||
updated_value[key] = new_value[key]
|
||||
|
||||
# We cover all current fields above, but just in case we start
|
||||
# supporting more fields in the future.
|
||||
updated_value.update(cur_value)
|
||||
updated_value.update(new_value)
|
||||
data[index] = updated_value
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Http views to control the config manager."""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
|
||||
|
||||
REQUIREMENTS = ['voluptuous-serialize==1']
|
||||
@@ -16,15 +15,17 @@ def async_setup(hass):
|
||||
"""Enable the Home Assistant views."""
|
||||
hass.http.register_view(ConfigManagerEntryIndexView)
|
||||
hass.http.register_view(ConfigManagerEntryResourceView)
|
||||
hass.http.register_view(ConfigManagerFlowIndexView)
|
||||
hass.http.register_view(ConfigManagerFlowResourceView)
|
||||
hass.http.register_view(
|
||||
ConfigManagerFlowIndexView(hass.config_entries.flow))
|
||||
hass.http.register_view(
|
||||
ConfigManagerFlowResourceView(hass.config_entries.flow))
|
||||
hass.http.register_view(ConfigManagerAvailableFlowView)
|
||||
return True
|
||||
|
||||
|
||||
def _prepare_json(result):
|
||||
"""Convert result for JSON."""
|
||||
if result['type'] != config_entries.RESULT_TYPE_FORM:
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
return result
|
||||
|
||||
import voluptuous_serialize
|
||||
@@ -78,7 +79,7 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
|
||||
return self.json(result)
|
||||
|
||||
|
||||
class ConfigManagerFlowIndexView(HomeAssistantView):
|
||||
class ConfigManagerFlowIndexView(FlowManagerIndexView):
|
||||
"""View to create config flows."""
|
||||
|
||||
url = '/api/config/config_entries/flow'
|
||||
@@ -94,81 +95,16 @@ class ConfigManagerFlowIndexView(HomeAssistantView):
|
||||
hass = request.app['hass']
|
||||
|
||||
return self.json([
|
||||
flow for flow in hass.config_entries.flow.async_progress()
|
||||
if flow['source'] != config_entries.SOURCE_USER])
|
||||
|
||||
@asyncio.coroutine
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('domain'): str,
|
||||
}))
|
||||
def post(self, request, data):
|
||||
"""Handle a POST request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
result = yield from hass.config_entries.flow.async_init(
|
||||
data['domain'])
|
||||
except config_entries.UnknownHandler:
|
||||
return self.json_message('Invalid handler specified', 404)
|
||||
except config_entries.UnknownStep:
|
||||
return self.json_message('Handler does not support init', 400)
|
||||
|
||||
result = _prepare_json(result)
|
||||
|
||||
return self.json(result)
|
||||
flw for flw in hass.config_entries.flow.async_progress()
|
||||
if flw['source'] != data_entry_flow.SOURCE_USER])
|
||||
|
||||
|
||||
class ConfigManagerFlowResourceView(HomeAssistantView):
|
||||
class ConfigManagerFlowResourceView(FlowManagerResourceView):
|
||||
"""View to interact with the flow manager."""
|
||||
|
||||
url = '/api/config/config_entries/flow/{flow_id}'
|
||||
name = 'api:config:config_entries:flow:resource'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, flow_id):
|
||||
"""Get the current state of a flow."""
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
result = yield from hass.config_entries.flow.async_configure(
|
||||
flow_id)
|
||||
except config_entries.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
result = _prepare_json(result)
|
||||
|
||||
return self.json(result)
|
||||
|
||||
@asyncio.coroutine
|
||||
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
|
||||
def post(self, request, flow_id, data):
|
||||
"""Handle a POST request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
result = yield from hass.config_entries.flow.async_configure(
|
||||
flow_id, data)
|
||||
except config_entries.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
except vol.Invalid:
|
||||
return self.json_message('User input malformed', 400)
|
||||
|
||||
result = _prepare_json(result)
|
||||
|
||||
return self.json(result)
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete(self, request, flow_id):
|
||||
"""Cancel a flow in progress."""
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
hass.config_entries.async_abort(flow_id)
|
||||
except config_entries.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
return self.json_message('Flow aborted')
|
||||
|
||||
|
||||
class ConfigManagerAvailableFlowView(HomeAssistantView):
|
||||
"""View to query available flows."""
|
||||
|
||||
55
homeassistant/components/config/entity_registry.py
Normal file
55
homeassistant/components/config/entity_registry.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""HTTP views to interact with the entity registry."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.helpers.entity_registry import async_get_registry
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Enable the Entity Registry views."""
|
||||
hass.http.register_view(ConfigManagerEntityView)
|
||||
return True
|
||||
|
||||
|
||||
class ConfigManagerEntityView(HomeAssistantView):
|
||||
"""View to interact with an entity registry entry."""
|
||||
|
||||
url = '/api/config/entity_registry/{entity_id}'
|
||||
name = 'api:config:entity_registry:entity'
|
||||
|
||||
async def get(self, request, entity_id):
|
||||
"""Get the entity registry settings for an entity."""
|
||||
hass = request.app['hass']
|
||||
registry = await async_get_registry(hass)
|
||||
entry = registry.entities.get(entity_id)
|
||||
|
||||
if entry is None:
|
||||
return self.json_message('Entry not found', 404)
|
||||
|
||||
return self.json(_entry_dict(entry))
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
# If passed in, we update value. Passing None will remove old value.
|
||||
vol.Optional('name'): vol.Any(str, None),
|
||||
}))
|
||||
async def post(self, request, entity_id, data):
|
||||
"""Update the entity registry settings for an entity."""
|
||||
hass = request.app['hass']
|
||||
registry = await async_get_registry(hass)
|
||||
|
||||
if entity_id not in registry.entities:
|
||||
return self.json_message('Entry not found', 404)
|
||||
|
||||
entry = registry.async_update_entity(entity_id, **data)
|
||||
return self.json(_entry_dict(entry))
|
||||
|
||||
|
||||
@callback
|
||||
def _entry_dict(entry):
|
||||
"""Helper to convert entry to API format."""
|
||||
return {
|
||||
'entity_id': entry.entity_id,
|
||||
'name': entry.name
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
"""Example component to show how config entries work."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.util import slugify
|
||||
|
||||
|
||||
DOMAIN = 'config_entry_example'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup for our example component."""
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_entry(hass, entry):
|
||||
"""Initialize an entry."""
|
||||
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
|
||||
hass.states.async_set(entity_id, 'loaded', {
|
||||
ATTR_FRIENDLY_NAME: entry.data['name']
|
||||
})
|
||||
|
||||
# Indicate setup was successful.
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_unload_entry(hass, entry):
|
||||
"""Unload an entry."""
|
||||
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
|
||||
hass.states.async_remove(entity_id)
|
||||
|
||||
# Indicate unload was successful.
|
||||
return True
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class ExampleConfigFlow(config_entries.ConfigFlowHandler):
|
||||
"""Handle an example configuration flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a Hue config handler."""
|
||||
self.object_id = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_step_init(self, user_input=None):
|
||||
"""Start config flow."""
|
||||
errors = None
|
||||
if user_input is not None:
|
||||
object_id = user_input['object_id']
|
||||
|
||||
if object_id != '' and object_id == slugify(object_id):
|
||||
self.object_id = user_input['object_id']
|
||||
return (yield from self.async_step_name())
|
||||
|
||||
errors = {
|
||||
'object_id': 'Invalid object id.'
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
title='Pick object id',
|
||||
step_id='init',
|
||||
description="Please enter an object_id for the test entity.",
|
||||
data_schema=vol.Schema({
|
||||
'object_id': str
|
||||
}),
|
||||
errors=errors
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_step_name(self, user_input=None):
|
||||
"""Ask user to enter the name."""
|
||||
errors = None
|
||||
if user_input is not None:
|
||||
name = user_input['name']
|
||||
|
||||
if name != '':
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={
|
||||
'name': name,
|
||||
'object_id': self.object_id,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
title='Name of the entity',
|
||||
step_id='name',
|
||||
description="Please enter a name for the test entity.",
|
||||
data_schema=vol.Schema({
|
||||
'name': str
|
||||
}),
|
||||
errors=errors
|
||||
)
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_KEY_INSTANCE = 'configurator'
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for functionality to have conversations with Home Assistant.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/conversation/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -14,10 +13,14 @@ from homeassistant import core
|
||||
from homeassistant.components import http
|
||||
from homeassistant.components.http.data_validator import (
|
||||
RequestDataValidator)
|
||||
from homeassistant.components.cover import (INTENT_OPEN_COVER,
|
||||
INTENT_CLOSE_COVER)
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import intent
|
||||
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.setup import (ATTR_COMPONENT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,6 +32,13 @@ DOMAIN = 'conversation'
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
REGEX_TYPE = type(re.compile(''))
|
||||
|
||||
UTTERANCES = {
|
||||
'cover': {
|
||||
INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'],
|
||||
INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]']
|
||||
}
|
||||
}
|
||||
|
||||
SERVICE_PROCESS = 'process'
|
||||
|
||||
SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
@@ -67,8 +77,7 @@ def async_register(hass, intent_type, utterances):
|
||||
conf.append(_create_matcher(utterance))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Register the process service."""
|
||||
config = config.get(DOMAIN, {})
|
||||
intents = hass.data.get(DOMAIN)
|
||||
@@ -84,49 +93,92 @@ def async_setup(hass, config):
|
||||
|
||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
||||
|
||||
@asyncio.coroutine
|
||||
def process(service):
|
||||
async def process(service):
|
||||
"""Parse text into commands."""
|
||||
text = service.data[ATTR_TEXT]
|
||||
yield from _process(hass, text)
|
||||
try:
|
||||
await _process(hass, text)
|
||||
except intent.IntentHandleError as err:
|
||||
_LOGGER.error('Error processing %s: %s', text, err)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
|
||||
|
||||
hass.http.register_view(ConversationProcessView)
|
||||
|
||||
async_register(hass, intent.INTENT_TURN_ON,
|
||||
['Turn {name} on', 'Turn on {name}'])
|
||||
async_register(hass, intent.INTENT_TURN_OFF,
|
||||
['Turn {name} off', 'Turn off {name}'])
|
||||
async_register(hass, intent.INTENT_TOGGLE,
|
||||
['Toggle {name}', '{name} toggle'])
|
||||
# We strip trailing 's' from name because our state matcher will fail
|
||||
# if a letter is not there. By removing 's' we can match singular and
|
||||
# plural names.
|
||||
|
||||
async_register(hass, intent.INTENT_TURN_ON, [
|
||||
'Turn [the] [a] {name}[s] on',
|
||||
'Turn on [the] [a] [an] {name}[s]',
|
||||
])
|
||||
async_register(hass, intent.INTENT_TURN_OFF, [
|
||||
'Turn [the] [a] [an] {name}[s] off',
|
||||
'Turn off [the] [a] [an] {name}[s]',
|
||||
])
|
||||
async_register(hass, intent.INTENT_TOGGLE, [
|
||||
'Toggle [the] [a] [an] {name}[s]',
|
||||
'[the] [a] [an] {name}[s] toggle',
|
||||
])
|
||||
|
||||
@callback
|
||||
def register_utterances(component):
|
||||
"""Register utterances for a component."""
|
||||
if component not in UTTERANCES:
|
||||
return
|
||||
for intent_type, sentences in UTTERANCES[component].items():
|
||||
async_register(hass, intent_type, sentences)
|
||||
|
||||
@callback
|
||||
def component_loaded(event):
|
||||
"""Handle a new component loaded."""
|
||||
register_utterances(event.data[ATTR_COMPONENT])
|
||||
|
||||
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
|
||||
|
||||
# Check already loaded components.
|
||||
for component in hass.config.components:
|
||||
register_utterances(component)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _create_matcher(utterance):
|
||||
"""Create a regex that matches the utterance."""
|
||||
parts = re.split(r'({\w+})', utterance)
|
||||
# Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
|
||||
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
|
||||
parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance)
|
||||
# Pattern to extract name from GROUP part. Matches {name}
|
||||
group_matcher = re.compile(r'{(\w+)}')
|
||||
# Pattern to extract text from OPTIONAL part. Matches [the color]
|
||||
optional_matcher = re.compile(r'\[([\w ]+)\] *')
|
||||
|
||||
pattern = ['^']
|
||||
|
||||
for part in parts:
|
||||
match = group_matcher.match(part)
|
||||
group_match = group_matcher.match(part)
|
||||
optional_match = optional_matcher.match(part)
|
||||
|
||||
if match is None:
|
||||
# Normal part
|
||||
if group_match is None and optional_match is None:
|
||||
pattern.append(part)
|
||||
continue
|
||||
|
||||
pattern.append('(?P<{}>{})'.format(match.groups()[0], r'[\w ]+'))
|
||||
# Group part
|
||||
if group_match is not None:
|
||||
pattern.append(
|
||||
r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0]))
|
||||
|
||||
# Optional part
|
||||
elif optional_match is not None:
|
||||
pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0]))
|
||||
|
||||
pattern.append('$')
|
||||
return re.compile(''.join(pattern), re.I)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _process(hass, text):
|
||||
async def _process(hass, text):
|
||||
"""Process a line of text."""
|
||||
intents = hass.data.get(DOMAIN, {})
|
||||
|
||||
@@ -137,7 +189,7 @@ def _process(hass, text):
|
||||
if not match:
|
||||
continue
|
||||
|
||||
response = yield from hass.helpers.intent.async_handle(
|
||||
response = await hass.helpers.intent.async_handle(
|
||||
DOMAIN, intent_type,
|
||||
{key: {'value': value} for key, value
|
||||
in match.groupdict().items()}, text)
|
||||
@@ -153,12 +205,15 @@ class ConversationProcessView(http.HomeAssistantView):
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('text'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Send a request for processing."""
|
||||
hass = request.app['hass']
|
||||
|
||||
intent_result = yield from _process(hass, data['text'])
|
||||
try:
|
||||
intent_result = await _process(hass, data['text'])
|
||||
except intent.IntentHandleError as err:
|
||||
intent_result = intent.IntentResponse()
|
||||
intent_result.async_set_speech(str(err))
|
||||
|
||||
if intent_result is None:
|
||||
intent_result = intent.IntentResponse()
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import group
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.const import (
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
|
||||
@@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
|
||||
ATTR_POSITION = 'position'
|
||||
ATTR_TILT_POSITION = 'tilt_position'
|
||||
|
||||
INTENT_OPEN_COVER = 'HassOpenCover'
|
||||
INTENT_CLOSE_COVER = 'HassCloseCover'
|
||||
|
||||
COVER_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
@@ -150,16 +154,14 @@ def stop_cover_tilt(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for covers."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
await component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_cover_service(service):
|
||||
async def async_handle_cover_service(service):
|
||||
"""Handle calls to the cover services."""
|
||||
covers = component.async_extract_from_service(service)
|
||||
method = SERVICE_TO_METHOD.get(service.service)
|
||||
@@ -169,13 +171,13 @@ def async_setup(hass, config):
|
||||
# call method
|
||||
update_tasks = []
|
||||
for cover in covers:
|
||||
yield from getattr(cover, method['method'])(**params)
|
||||
await getattr(cover, method['method'])(**params)
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
update_tasks.append(cover.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
for service_name in SERVICE_TO_METHOD:
|
||||
schema = SERVICE_TO_METHOD[service_name].get(
|
||||
@@ -183,6 +185,12 @@ def async_setup(hass, config):
|
||||
hass.services.async_register(
|
||||
DOMAIN, service_name, async_handle_cover_service,
|
||||
schema=schema)
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER,
|
||||
"Opened {}"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER,
|
||||
"Closed {}"))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
116
homeassistant/components/cover/gogogate2.py
Normal file
116
homeassistant/components/cover/gogogate2.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Support for Gogogate2 Garage Doors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/cover.gogogate2/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED,
|
||||
CONF_IP_ADDRESS, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pygogogate2==0.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'gogogate2'
|
||||
|
||||
NOTIFICATION_ID = 'gogogate2_notification'
|
||||
NOTIFICATION_TITLE = 'Gogogate2 Cover Setup'
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_IP_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Gogogate2 component."""
|
||||
from pygogogate2 import Gogogate2API as pygogogate2
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
ip_address = config.get(CONF_IP_ADDRESS)
|
||||
name = config.get(CONF_NAME)
|
||||
mygogogate2 = pygogogate2(username, password, ip_address)
|
||||
|
||||
try:
|
||||
devices = mygogogate2.get_devices()
|
||||
if devices is False:
|
||||
raise ValueError(
|
||||
"Username or Password is incorrect or no devices found")
|
||||
|
||||
add_devices(MyGogogate2Device(
|
||||
mygogogate2, door, name) for door in devices)
|
||||
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
|
||||
|
||||
class MyGogogate2Device(CoverDevice):
|
||||
"""Representation of a Gogogate2 cover."""
|
||||
|
||||
def __init__(self, mygogogate2, device, name):
|
||||
"""Initialize with API object, device id."""
|
||||
self.mygogogate2 = mygogogate2
|
||||
self.device_id = device['door']
|
||||
self._name = name or device['name']
|
||||
self._status = device['status']
|
||||
self._available = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door if any."""
|
||||
return self._name if self._name else DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if cover is closed, else False."""
|
||||
return self._status == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'garage'
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Could the device be accessed during the last update call."""
|
||||
return self._available
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Issue close command to cover."""
|
||||
self.mygogogate2.close_device(self.device_id)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Issue open command to cover."""
|
||||
self.mygogogate2.open_device(self.device_id)
|
||||
|
||||
def update(self):
|
||||
"""Update status of cover."""
|
||||
try:
|
||||
self._status = self.mygogogate2.get_status(self.device_id)
|
||||
self._available = True
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
self._status = None
|
||||
self._available = False
|
||||
271
homeassistant/components/cover/group.py
Executable file
271
homeassistant/components/cover/group.py
Executable file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
This platform allows several cover to be grouped into one cover.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.group/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN, PLATFORM_SCHEMA, CoverDevice, ATTR_POSITION,
|
||||
ATTR_CURRENT_POSITION, ATTR_TILT_POSITION, ATTR_CURRENT_TILT_POSITION,
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION,
|
||||
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT,
|
||||
SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION,
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
|
||||
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION)
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
|
||||
CONF_ENTITIES, CONF_NAME, STATE_CLOSED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
KEY_OPEN_CLOSE = 'open_close'
|
||||
KEY_STOP = 'stop'
|
||||
KEY_POSITION = 'position'
|
||||
|
||||
DEFAULT_NAME = 'Cover Group'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN),
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Group Cover platform."""
|
||||
async_add_devices(
|
||||
[CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])])
|
||||
|
||||
|
||||
class CoverGroup(CoverDevice):
|
||||
"""Representation of a CoverGroup."""
|
||||
|
||||
def __init__(self, name, entities):
|
||||
"""Initialize a CoverGroup entity."""
|
||||
self._name = name
|
||||
self._is_closed = False
|
||||
self._cover_position = 100
|
||||
self._tilt_position = None
|
||||
self._supported_features = 0
|
||||
self._assumed_state = True
|
||||
|
||||
self._entities = entities
|
||||
self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(),
|
||||
KEY_POSITION: set()}
|
||||
self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(),
|
||||
KEY_POSITION: set()}
|
||||
|
||||
@callback
|
||||
def update_supported_features(self, entity_id, old_state, new_state,
|
||||
update_state=True):
|
||||
"""Update dictionaries with supported features."""
|
||||
if not new_state:
|
||||
for values in self._covers.values():
|
||||
values.discard(entity_id)
|
||||
for values in self._tilts.values():
|
||||
values.discard(entity_id)
|
||||
if update_state:
|
||||
self.async_schedule_update_ha_state(True)
|
||||
return
|
||||
|
||||
features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if features & (SUPPORT_OPEN | SUPPORT_CLOSE):
|
||||
self._covers[KEY_OPEN_CLOSE].add(entity_id)
|
||||
else:
|
||||
self._covers[KEY_OPEN_CLOSE].discard(entity_id)
|
||||
if features & (SUPPORT_STOP):
|
||||
self._covers[KEY_STOP].add(entity_id)
|
||||
else:
|
||||
self._covers[KEY_STOP].discard(entity_id)
|
||||
if features & (SUPPORT_SET_POSITION):
|
||||
self._covers[KEY_POSITION].add(entity_id)
|
||||
else:
|
||||
self._covers[KEY_POSITION].discard(entity_id)
|
||||
|
||||
if features & (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT):
|
||||
self._tilts[KEY_OPEN_CLOSE].add(entity_id)
|
||||
else:
|
||||
self._tilts[KEY_OPEN_CLOSE].discard(entity_id)
|
||||
if features & (SUPPORT_STOP_TILT):
|
||||
self._tilts[KEY_STOP].add(entity_id)
|
||||
else:
|
||||
self._tilts[KEY_STOP].discard(entity_id)
|
||||
if features & (SUPPORT_SET_TILT_POSITION):
|
||||
self._tilts[KEY_POSITION].add(entity_id)
|
||||
else:
|
||||
self._tilts[KEY_POSITION].discard(entity_id)
|
||||
|
||||
if update_state:
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register listeners."""
|
||||
for entity_id in self._entities:
|
||||
new_state = self.hass.states.get(entity_id)
|
||||
self.update_supported_features(entity_id, None, new_state,
|
||||
update_state=False)
|
||||
async_track_state_change(self.hass, self._entities,
|
||||
self.update_supported_features)
|
||||
await self.async_update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Enable buttons even if at end position."""
|
||||
return self._assumed_state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling for cover group."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features for the cover."""
|
||||
return self._supported_features
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if all covers in group are closed."""
|
||||
return self._is_closed
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position for all covers."""
|
||||
return self._cover_position
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current tilt position for all covers."""
|
||||
return self._tilt_position
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Move the covers up."""
|
||||
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_OPEN_COVER, data, blocking=True)
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Move the covers down."""
|
||||
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True)
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_STOP_COVER, data, blocking=True)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Set covers position."""
|
||||
data = {ATTR_ENTITY_ID: self._covers[KEY_POSITION],
|
||||
ATTR_POSITION: kwargs[ATTR_POSITION]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True)
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs):
|
||||
"""Tilt covers open."""
|
||||
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True)
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs):
|
||||
"""Tilt covers closed."""
|
||||
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True)
|
||||
|
||||
async def async_stop_cover_tilt(self, **kwargs):
|
||||
"""Stop cover tilt."""
|
||||
data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True)
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Set tilt position."""
|
||||
data = {ATTR_ENTITY_ID: self._tilts[KEY_POSITION],
|
||||
ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update state and attributes."""
|
||||
self._assumed_state = False
|
||||
|
||||
self._is_closed = True
|
||||
for entity_id in self._entities:
|
||||
state = self.hass.states.get(entity_id)
|
||||
if not state:
|
||||
continue
|
||||
if state.state != STATE_CLOSED:
|
||||
self._is_closed = False
|
||||
break
|
||||
|
||||
self._cover_position = None
|
||||
if self._covers[KEY_POSITION]:
|
||||
position = -1
|
||||
self._cover_position = 0 if self.is_closed else 100
|
||||
for entity_id in self._covers[KEY_POSITION]:
|
||||
state = self.hass.states.get(entity_id)
|
||||
pos = state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if position == -1:
|
||||
position = pos
|
||||
elif position != pos:
|
||||
self._assumed_state = True
|
||||
break
|
||||
else:
|
||||
if position != -1:
|
||||
self._cover_position = position
|
||||
|
||||
self._tilt_position = None
|
||||
if self._tilts[KEY_POSITION]:
|
||||
position = -1
|
||||
self._tilt_position = 100
|
||||
for entity_id in self._tilts[KEY_POSITION]:
|
||||
state = self.hass.states.get(entity_id)
|
||||
pos = state.attributes.get(ATTR_CURRENT_TILT_POSITION)
|
||||
if position == -1:
|
||||
position = pos
|
||||
elif position != pos:
|
||||
self._assumed_state = True
|
||||
break
|
||||
else:
|
||||
if position != -1:
|
||||
self._tilt_position = position
|
||||
|
||||
supported_features = 0
|
||||
supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE \
|
||||
if self._covers[KEY_OPEN_CLOSE] else 0
|
||||
supported_features |= SUPPORT_STOP \
|
||||
if self._covers[KEY_STOP] else 0
|
||||
supported_features |= SUPPORT_SET_POSITION \
|
||||
if self._covers[KEY_POSITION] else 0
|
||||
supported_features |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT \
|
||||
if self._tilts[KEY_OPEN_CLOSE] else 0
|
||||
supported_features |= SUPPORT_STOP_TILT \
|
||||
if self._tilts[KEY_STOP] else 0
|
||||
supported_features |= SUPPORT_SET_TILT_POSITION \
|
||||
if self._tilts[KEY_POSITION] else 0
|
||||
self._supported_features = supported_features
|
||||
|
||||
if not self._assumed_state:
|
||||
for entity_id in self._entities:
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state and state.attributes.get(ATTR_ASSUMED_STATE):
|
||||
self._assumed_state = True
|
||||
break
|
||||
@@ -4,7 +4,6 @@ Support for KNX/IP covers.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.knx/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -50,8 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up cover(s) for KNX platform."""
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
@@ -106,11 +105,10 @@ class KNXCover(CoverDevice):
|
||||
@callback
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
@@ -147,32 +145,28 @@ class KNXCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
return self.device.is_closed()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
if not self.device.is_closed():
|
||||
yield from self.device.set_down()
|
||||
await self.device.set_down()
|
||||
self.start_auto_updater()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
if not self.device.is_open():
|
||||
yield from self.device.set_up()
|
||||
await self.device.set_up()
|
||||
self.start_auto_updater()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
yield from self.device.set_position(position)
|
||||
await self.device.set_position(position)
|
||||
self.start_auto_updater()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
yield from self.device.stop()
|
||||
await self.device.stop()
|
||||
self.stop_auto_updater()
|
||||
|
||||
@property
|
||||
@@ -182,12 +176,11 @@ class KNXCover(CoverDevice):
|
||||
return None
|
||||
return self.device.current_angle()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
async def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if ATTR_TILT_POSITION in kwargs:
|
||||
tilt_position = kwargs[ATTR_TILT_POSITION]
|
||||
yield from self.device.set_angle(tilt_position)
|
||||
await self.device.set_angle(tilt_position)
|
||||
|
||||
def start_auto_updater(self):
|
||||
"""Start the autoupdater to update HASS while cover is moving."""
|
||||
|
||||
@@ -9,10 +9,12 @@ from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the MySensors platform for covers."""
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the mysensors platform for covers."""
|
||||
mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices)
|
||||
hass, DOMAIN, discovery_info, MySensorsCover,
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
|
||||
@@ -18,30 +18,31 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DISTANCE_SENSOR = "distance_sensor"
|
||||
ATTR_DOOR_STATE = "door_state"
|
||||
ATTR_SIGNAL_STRENGTH = "wifi_signal"
|
||||
ATTR_DISTANCE_SENSOR = 'distance_sensor'
|
||||
ATTR_DOOR_STATE = 'door_state'
|
||||
ATTR_SIGNAL_STRENGTH = 'wifi_signal'
|
||||
|
||||
CONF_DEVICEKEY = "device_key"
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
CONF_DEVICE_KEY = 'device_key'
|
||||
|
||||
DEFAULT_NAME = 'OpenGarage'
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
STATE_CLOSING = "closing"
|
||||
STATE_OFFLINE = "offline"
|
||||
STATE_OPENING = "opening"
|
||||
STATE_STOPPED = "stopped"
|
||||
STATE_CLOSING = 'closing'
|
||||
STATE_OFFLINE = 'offline'
|
||||
STATE_OPENING = 'opening'
|
||||
STATE_STOPPED = 'stopped'
|
||||
|
||||
STATES_MAP = {
|
||||
0: STATE_CLOSED,
|
||||
1: STATE_OPEN
|
||||
1: STATE_OPEN,
|
||||
}
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICEKEY): cv.string,
|
||||
vol.Required(CONF_DEVICE_KEY): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_NAME): cv.string
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -50,7 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up OpenGarage covers."""
|
||||
"""Set up the OpenGarage covers."""
|
||||
covers = []
|
||||
devices = config.get(CONF_COVERS)
|
||||
|
||||
@@ -59,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
CONF_NAME: device_config.get(CONF_NAME),
|
||||
CONF_HOST: device_config.get(CONF_HOST),
|
||||
CONF_PORT: device_config.get(CONF_PORT),
|
||||
"device_id": device_config.get(CONF_DEVICE, device_id),
|
||||
CONF_DEVICEKEY: device_config.get(CONF_DEVICEKEY)
|
||||
CONF_DEVICE_ID: device_config.get(CONF_DEVICE, device_id),
|
||||
CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY)
|
||||
}
|
||||
|
||||
covers.append(OpenGarageCover(hass, args))
|
||||
@@ -79,8 +80,8 @@ class OpenGarageCover(CoverDevice):
|
||||
self.hass = hass
|
||||
self._name = args[CONF_NAME]
|
||||
self.device_id = args['device_id']
|
||||
self._devicekey = args[CONF_DEVICEKEY]
|
||||
self._state = STATE_UNKNOWN
|
||||
self._device_key = args[CONF_DEVICE_KEY]
|
||||
self._state = None
|
||||
self._state_before_move = None
|
||||
self.dist = None
|
||||
self.signal = None
|
||||
@@ -138,8 +139,8 @@ class OpenGarageCover(CoverDevice):
|
||||
try:
|
||||
status = self._get_status()
|
||||
if self._name is None:
|
||||
if status["name"] is not None:
|
||||
self._name = status["name"]
|
||||
if status['name'] is not None:
|
||||
self._name = status['name']
|
||||
state = STATES_MAP.get(status.get('door'), STATE_UNKNOWN)
|
||||
if self._state_before_move is not None:
|
||||
if self._state_before_move != state:
|
||||
@@ -152,7 +153,7 @@ class OpenGarageCover(CoverDevice):
|
||||
self.signal = status.get('rssi')
|
||||
self.dist = status.get('dist')
|
||||
self._available = True
|
||||
except (requests.exceptions.RequestException) as ex:
|
||||
except requests.exceptions.RequestException as ex:
|
||||
_LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
|
||||
dict(reason=ex))
|
||||
self._state = STATE_OFFLINE
|
||||
@@ -166,15 +167,15 @@ class OpenGarageCover(CoverDevice):
|
||||
def _push_button(self):
|
||||
"""Send commands to API."""
|
||||
url = '{}/cc?dkey={}&click=1'.format(
|
||||
self.opengarage_url, self._devicekey)
|
||||
self.opengarage_url, self._device_key)
|
||||
try:
|
||||
response = requests.get(url, timeout=10).json()
|
||||
if response["result"] == 2:
|
||||
_LOGGER.error("Unable to control %s: device_key is incorrect.",
|
||||
if response['result'] == 2:
|
||||
_LOGGER.error("Unable to control %s: Device key is incorrect",
|
||||
self._name)
|
||||
self._state = self._state_before_move
|
||||
self._state_before_move = None
|
||||
except (requests.exceptions.RequestException) as ex:
|
||||
except requests.exceptions.RequestException as ex:
|
||||
_LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
|
||||
dict(reason=ex))
|
||||
self._state = self._state_before_move
|
||||
|
||||
@@ -87,7 +87,7 @@ class RPiGPIOCover(CoverDevice):
|
||||
self._invert_relay = invert_relay
|
||||
rpi_gpio.setup_output(self._relay_pin)
|
||||
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
|
||||
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
|
||||
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -105,9 +105,9 @@ class RPiGPIOCover(CoverDevice):
|
||||
|
||||
def _trigger(self):
|
||||
"""Trigger the cover."""
|
||||
rpi_gpio.write_output(self._relay_pin, self._invert_relay)
|
||||
rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0)
|
||||
sleep(self._relay_time)
|
||||
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
|
||||
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
|
||||
@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Tahoma covers."""
|
||||
"""Set up the Tahoma covers."""
|
||||
controller = hass.data[TAHOMA_DOMAIN]['controller']
|
||||
devices = []
|
||||
for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']:
|
||||
|
||||
@@ -234,7 +234,9 @@ class CoverTemplate(CoverDevice):
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._position
|
||||
if self._position_template or self._position_script:
|
||||
return self._position
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
|
||||
26
homeassistant/components/deconz/.translations/en.json
Normal file
26
homeassistant/components/deconz/.translations/en.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "deCONZ",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Define deCONZ gateway",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (default value: '80')"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link with deCONZ",
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Couldn't get an API key"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Bridge is already configured",
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"one_instance_only": "Component only supports one deCONZ instance"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,29 +4,20 @@ Support for deCONZ devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/deconz/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.discovery import SERVICE_DECONZ
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client, discovery, config_validation as cv)
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
REQUIREMENTS = ['pydeconz==30']
|
||||
# Loading the config flow file will register the flow
|
||||
from .config_flow import configured_hosts
|
||||
from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'deconz'
|
||||
DATA_DECONZ_ID = 'deconz_entities'
|
||||
|
||||
CONFIG_FILE = 'deconz.conf'
|
||||
REQUIREMENTS = ['pydeconz==36']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@@ -47,50 +38,39 @@ SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
CONFIG_INSTRUCTIONS = """
|
||||
Unlock your deCONZ gateway to register with Home Assistant.
|
||||
|
||||
1. [Go to deCONZ system settings](http://{}:{}/edit_system.html)
|
||||
2. Press "Unlock Gateway" button
|
||||
|
||||
[deCONZ platform documentation](https://home-assistant.io/components/deconz/)
|
||||
"""
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up services and configuration for deCONZ component."""
|
||||
result = False
|
||||
config_file = yield from hass.async_add_job(
|
||||
load_json, hass.config.path(CONFIG_FILE))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_deconz_discovered(service, discovery_info):
|
||||
"""Call when deCONZ gateway has been found."""
|
||||
deconz_config = {}
|
||||
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
|
||||
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
|
||||
yield from async_request_configuration(hass, config, deconz_config)
|
||||
|
||||
if config_file:
|
||||
result = yield from async_setup_deconz(hass, config, config_file)
|
||||
|
||||
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
|
||||
deconz_config = config[DOMAIN]
|
||||
if CONF_API_KEY in deconz_config:
|
||||
result = yield from async_setup_deconz(hass, config, deconz_config)
|
||||
else:
|
||||
yield from async_request_configuration(hass, config, deconz_config)
|
||||
return True
|
||||
|
||||
if not result:
|
||||
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
|
||||
async def async_setup(hass, config):
|
||||
"""Load configuration for deCONZ component.
|
||||
|
||||
Discovery has loaded the component if DOMAIN is not present in config.
|
||||
"""
|
||||
if DOMAIN in config:
|
||||
deconz_config = None
|
||||
config_file = await hass.async_add_job(
|
||||
load_json, hass.config.path(CONFIG_FILE))
|
||||
if config_file:
|
||||
deconz_config = config_file
|
||||
elif CONF_HOST in config[DOMAIN]:
|
||||
deconz_config = config[DOMAIN]
|
||||
if deconz_config and not configured_hosts(hass):
|
||||
hass.async_add_job(hass.config_entries.flow.async_init(
|
||||
DOMAIN, source='import', data=deconz_config
|
||||
))
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_deconz(hass, config, deconz_config):
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up a deCONZ bridge for a config entry."""
|
||||
if DOMAIN in hass.data:
|
||||
_LOGGER.error(
|
||||
"Config entry failed since one deCONZ instance already exists")
|
||||
return False
|
||||
result = await async_setup_deconz(hass, None, entry.data)
|
||||
if result:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def async_setup_deconz(hass, config, deconz_config):
|
||||
"""Set up a deCONZ session.
|
||||
|
||||
Load config, group, light and sensor data for server information.
|
||||
@@ -98,9 +78,9 @@ def async_setup_deconz(hass, config, deconz_config):
|
||||
"""
|
||||
_LOGGER.debug("deCONZ config %s", deconz_config)
|
||||
from pydeconz import DeconzSession
|
||||
websession = async_get_clientsession(hass)
|
||||
deconz = DeconzSession(hass.loop, websession, **deconz_config)
|
||||
result = yield from deconz.async_load_parameters()
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
deconz = DeconzSession(hass.loop, session, **deconz_config)
|
||||
result = await deconz.async_load_parameters()
|
||||
if result is False:
|
||||
_LOGGER.error("Failed to communicate with deCONZ")
|
||||
return False
|
||||
@@ -113,8 +93,7 @@ def async_setup_deconz(hass, config, deconz_config):
|
||||
hass, component, DOMAIN, {}, config))
|
||||
deconz.start()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_configure(call):
|
||||
async def async_configure(call):
|
||||
"""Set attribute of device in deCONZ.
|
||||
|
||||
Field is a string representing a specific device in deCONZ
|
||||
@@ -140,7 +119,7 @@ def async_setup_deconz(hass, config, deconz_config):
|
||||
if field is None:
|
||||
_LOGGER.error('Could not find the entity %s', entity_id)
|
||||
return
|
||||
yield from deconz.async_put_state(field, data)
|
||||
await deconz.async_put_state(field, data)
|
||||
hass.services.async_register(
|
||||
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
|
||||
|
||||
@@ -157,40 +136,3 @@ def async_setup_deconz(hass, config, deconz_config):
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_request_configuration(hass, config, deconz_config):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_configuration_callback(data):
|
||||
"""Set up actions to do when our configuration callback is called."""
|
||||
from pydeconz.utils import async_get_api_key
|
||||
api_key = yield from async_get_api_key(hass.loop, **deconz_config)
|
||||
if api_key:
|
||||
deconz_config[CONF_API_KEY] = api_key
|
||||
result = yield from async_setup_deconz(hass, config, deconz_config)
|
||||
if result:
|
||||
yield from hass.async_add_job(
|
||||
save_json, hass.config.path(CONFIG_FILE), deconz_config)
|
||||
configurator.async_request_done(request_id)
|
||||
return
|
||||
else:
|
||||
configurator.async_notify_errors(
|
||||
request_id, "Couldn't load configuration.")
|
||||
else:
|
||||
configurator.async_notify_errors(
|
||||
request_id, "Couldn't get an API key.")
|
||||
return
|
||||
|
||||
instructions = CONFIG_INSTRUCTIONS.format(
|
||||
deconz_config[CONF_HOST], deconz_config[CONF_PORT])
|
||||
|
||||
request_id = configurator.async_request_config(
|
||||
"deCONZ", async_configuration_callback,
|
||||
description=instructions,
|
||||
entity_picture="/static/images/logo_deconz.jpeg",
|
||||
submit_caption="I have unlocked the gateway",
|
||||
)
|
||||
|
||||
139
homeassistant/components/deconz/config_flow.py
Normal file
139
homeassistant/components/deconz/config_flow.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Config flow to configure deCONZ component."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
from .const import CONFIG_FILE, DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def configured_hosts(hass):
|
||||
"""Return a set of the configured hosts."""
|
||||
return set(entry.data['host'] for entry
|
||||
in hass.config_entries.async_entries(DOMAIN))
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
"""Handle a deCONZ config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the deCONZ config flow."""
|
||||
self.bridges = []
|
||||
self.deconz_config = {}
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle a deCONZ config flow start."""
|
||||
from pydeconz.utils import async_discovery
|
||||
|
||||
if configured_hosts(self.hass):
|
||||
return self.async_abort(reason='one_instance_only')
|
||||
|
||||
if user_input is not None:
|
||||
for bridge in self.bridges:
|
||||
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
||||
self.deconz_config = bridge
|
||||
return await self.async_step_link()
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
self.bridges = await async_discovery(session)
|
||||
|
||||
if len(self.bridges) == 1:
|
||||
self.deconz_config = self.bridges[0]
|
||||
return await self.async_step_link()
|
||||
elif len(self.bridges) > 1:
|
||||
hosts = []
|
||||
for bridge in self.bridges:
|
||||
hosts.append(bridge[CONF_HOST])
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST): vol.In(hosts)
|
||||
})
|
||||
)
|
||||
|
||||
return self.async_abort(
|
||||
reason='no_bridges'
|
||||
)
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Attempt to link with the deCONZ bridge."""
|
||||
from pydeconz.utils import async_get_api_key, async_get_bridgeid
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
if configured_hosts(self.hass):
|
||||
return self.async_abort(reason='one_instance_only')
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
api_key = await async_get_api_key(session, **self.deconz_config)
|
||||
if api_key:
|
||||
self.deconz_config[CONF_API_KEY] = api_key
|
||||
if 'bridgeid' not in self.deconz_config:
|
||||
self.deconz_config['bridgeid'] = await async_get_bridgeid(
|
||||
session, **self.deconz_config)
|
||||
return self.async_create_entry(
|
||||
title='deCONZ-' + self.deconz_config['bridgeid'],
|
||||
data=self.deconz_config
|
||||
)
|
||||
errors['base'] = 'no_key'
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='link',
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Prepare configuration for a discovered deCONZ bridge.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
"""
|
||||
deconz_config = {}
|
||||
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
|
||||
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
|
||||
deconz_config['bridgeid'] = discovery_info.get('serial')
|
||||
|
||||
config_file = await self.hass.async_add_job(
|
||||
load_json, self.hass.config.path(CONFIG_FILE))
|
||||
if config_file and \
|
||||
config_file[CONF_HOST] == deconz_config[CONF_HOST] and \
|
||||
CONF_API_KEY in config_file:
|
||||
deconz_config[CONF_API_KEY] = config_file[CONF_API_KEY]
|
||||
|
||||
return await self.async_step_import(deconz_config)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import a deCONZ bridge as a config entry.
|
||||
|
||||
This flow is triggered by `async_setup` for configured bridges.
|
||||
This flow is also triggered by `async_step_discovery`.
|
||||
|
||||
This will execute for any bridge that does not have a
|
||||
config entry yet (based on host).
|
||||
|
||||
If an API key is provided, we will create an entry.
|
||||
Otherwise we will delegate to `link` step which
|
||||
will ask user to link the bridge.
|
||||
"""
|
||||
from pydeconz.utils import async_get_bridgeid
|
||||
|
||||
if configured_hosts(self.hass):
|
||||
return self.async_abort(reason='one_instance_only')
|
||||
elif CONF_API_KEY not in import_config:
|
||||
self.deconz_config = import_config
|
||||
return await self.async_step_link()
|
||||
|
||||
if 'bridgeid' not in import_config:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
import_config['bridgeid'] = await async_get_bridgeid(
|
||||
session, **import_config)
|
||||
return self.async_create_entry(
|
||||
title='deCONZ-' + import_config['bridgeid'],
|
||||
data=import_config
|
||||
)
|
||||
8
homeassistant/components/deconz/const.py
Normal file
8
homeassistant/components/deconz/const.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Constants for the deCONZ component."""
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger('homeassistant.components.deconz')
|
||||
|
||||
DOMAIN = 'deconz'
|
||||
CONFIG_FILE = 'deconz.conf'
|
||||
DATA_DECONZ_ID = 'deconz_entities'
|
||||
26
homeassistant/components/deconz/strings.json
Normal file
26
homeassistant/components/deconz/strings.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "deCONZ",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Define deCONZ gateway",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (default value: '80')"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link with deCONZ",
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Couldn't get an API key"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Bridge is already configured",
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"one_instance_only": "Component only supports one deCONZ instance"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user