mirror of
https://github.com/home-assistant/core.git
synced 2026-01-04 23:05:26 +01:00
Compare commits
573 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
d8a7c547df | ||
|
|
ecaf0189cc | ||
|
|
ca5f470956 | ||
|
|
3ba19c52d5 | ||
|
|
6734c966b3 | ||
|
|
5e2296f2a4 | ||
|
|
c5228cbc64 |
65
.coveragerc
65
.coveragerc
@@ -94,6 +94,12 @@ omit =
|
||||
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
|
||||
|
||||
@@ -106,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
|
||||
|
||||
@@ -157,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
|
||||
|
||||
@@ -190,8 +199,8 @@ omit =
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/qwikswitch.py
|
||||
homeassistant/components/*/qwikswitch.py
|
||||
homeassistant/components/switch/qwikswitch.py
|
||||
homeassistant/components/light/qwikswitch.py
|
||||
|
||||
homeassistant/components/rachio.py
|
||||
homeassistant/components/*/rachio.py
|
||||
@@ -286,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
|
||||
@@ -309,6 +316,7 @@ omit =
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.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
|
||||
@@ -332,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
|
||||
@@ -352,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
|
||||
@@ -369,6 +379,7 @@ omit =
|
||||
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
|
||||
@@ -395,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
|
||||
@@ -427,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
|
||||
@@ -436,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
|
||||
@@ -482,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
|
||||
@@ -491,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
|
||||
@@ -502,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
|
||||
@@ -518,6 +532,7 @@ 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
|
||||
@@ -531,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
|
||||
@@ -554,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
|
||||
@@ -576,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
|
||||
@@ -588,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
|
||||
@@ -632,9 +646,11 @@ omit =
|
||||
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
|
||||
@@ -647,6 +663,7 @@ 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
|
||||
@@ -656,15 +673,18 @@ omit =
|
||||
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
|
||||
@@ -697,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
|
||||
@@ -706,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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,6 +21,7 @@ Icon
|
||||
*.iml
|
||||
|
||||
# pytest
|
||||
.pytest_cache
|
||||
.cache
|
||||
|
||||
# GITHUB Proposed Python stuff:
|
||||
|
||||
0
.gitmodules
vendored
0
.gitmodules
vendored
@@ -31,7 +31,7 @@ script: travis_wait 30 tox --develop
|
||||
services:
|
||||
- docker
|
||||
before_deploy:
|
||||
- docker pull lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7
|
||||
- docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21
|
||||
deploy:
|
||||
skip_cleanup: true
|
||||
provider: script
|
||||
|
||||
23
CODEOWNERS
23
CODEOWNERS
@@ -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,12 +40,14 @@ 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
|
||||
@@ -64,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
|
||||
@@ -79,17 +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
|
||||
@@ -97,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:
|
||||
|
||||
@@ -126,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',
|
||||
@@ -259,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."""
|
||||
@@ -335,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, {})
|
||||
|
||||
@@ -119,6 +114,11 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
conf_util.merge_packages_config(
|
||||
config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||
|
||||
# Ensure we have no None values after merge
|
||||
for key, value in config.items():
|
||||
if not value:
|
||||
config[key] = {}
|
||||
|
||||
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
||||
yield from hass.config_entries.async_load()
|
||||
|
||||
@@ -167,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,
|
||||
@@ -179,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
|
||||
@@ -191,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.
|
||||
@@ -202,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(
|
||||
@@ -219,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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -12,13 +12,14 @@ import requests
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
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
|
||||
)
|
||||
REQUIREMENTS = ['pythonegardia==1.0.38']
|
||||
DEPENDENCIES = ['egardia']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,12 +28,16 @@ STATES = {
|
||||
'DAY HOME': STATE_ALARM_ARMED_HOME,
|
||||
'DISARM': STATE_ALARM_DISARMED,
|
||||
'ARMHOME': STATE_ALARM_ARMED_HOME,
|
||||
'HOME': STATE_ALARM_ARMED_HOME,
|
||||
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
|
||||
'TRIGGERED': STATE_ALARM_TRIGGERED
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Egardia platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
device = EgardiaAlarm(
|
||||
discovery_info['name'],
|
||||
hass.data[EGARDIA_DEVICE],
|
||||
|
||||
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
|
||||
|
||||
@@ -356,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."""
|
||||
|
||||
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)
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ 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'}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
'motionSensor': 'motion',
|
||||
'doorSensor': 'door',
|
||||
'leakSensor': 'moisture'}
|
||||
'wetLeakSensor': 'moisture'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
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.debug('Adding device %s entity %s to Binary Sensor platform',
|
||||
device.address.hex, device.states[state_key].name)
|
||||
new_entity = InsteonPLMBinarySensor(device, state_key)
|
||||
|
||||
new_entity = InsteonPLMBinarySensor(device, state_key)
|
||||
|
||||
async_add_devices([new_entity])
|
||||
async_add_devices([new_entity])
|
||||
|
||||
|
||||
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
@@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
sensorstate = self._insteon_device_state.value
|
||||
return bool(sensorstate)
|
||||
return bool(self._insteon_device_state.value)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -103,92 +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()
|
||||
self._profiles = media_service.GetProfiles()
|
||||
self._profile_index = config.get(CONF_PROFILE)
|
||||
if self._profile_index >= len(self._profiles):
|
||||
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 = media_service.create_type('GetStreamUri')
|
||||
|
||||
req = self._media_service.create_type('GetStreamUri')
|
||||
|
||||
# pylint: disable=protected-access
|
||||
req.ProfileToken = self._profiles[self._profile_index]._token
|
||||
self._input = media_service.GetStreamUri(req).Uri.replace(
|
||||
'rtsp://', 'rtsp://{}:{}@'.format(
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD)), 1)
|
||||
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):
|
||||
|
||||
@@ -11,7 +11,7 @@ import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
async_add_devices([ProxyCamera(hass, config)])
|
||||
|
||||
|
||||
async def _read_frame(req):
|
||||
"""Read a single frame from an MJPEG stream."""
|
||||
# based on https://gist.github.com/russss/1143799
|
||||
import cgi
|
||||
# Read in HTTP headers:
|
||||
stream = req.content
|
||||
# multipart/x-mixed-replace; boundary=--frameboundary
|
||||
_mimetype, options = cgi.parse_header(req.headers['content-type'])
|
||||
boundary = options.get('boundary').encode('utf-8')
|
||||
if not boundary:
|
||||
_LOGGER.error("Malformed MJPEG missing boundary")
|
||||
raise Exception("Can't find content-type")
|
||||
|
||||
line = await stream.readline()
|
||||
# Seek ahead to the first chunk
|
||||
while line.strip() != boundary:
|
||||
line = await stream.readline()
|
||||
# Read in chunk headers
|
||||
while line.strip() != b'':
|
||||
parts = line.split(b':')
|
||||
if len(parts) > 1 and parts[0].lower() == b'content-length':
|
||||
# Grab chunk length
|
||||
length = int(parts[1].strip())
|
||||
line = await stream.readline()
|
||||
image = await stream.read(length)
|
||||
return image
|
||||
|
||||
|
||||
def _resize_image(image, opts):
|
||||
"""Resize image."""
|
||||
from PIL import Image
|
||||
@@ -227,9 +199,9 @@ class ProxyCamera(Camera):
|
||||
'boundary=--frameboundary')
|
||||
await 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(
|
||||
@@ -240,13 +212,23 @@ class ProxyCamera(Camera):
|
||||
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:
|
||||
image = await _read_frame(req)
|
||||
if not image:
|
||||
chunk = await stream.read(102400)
|
||||
if not chunk:
|
||||
break
|
||||
image = await self.hass.async_add_job(
|
||||
_resize_image, image, self._stream_opts)
|
||||
write(image)
|
||||
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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.1']
|
||||
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'
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -179,7 +179,7 @@ class NestThermostat(ClimateDevice):
|
||||
try:
|
||||
self.device.target = temp
|
||||
except nest.nest.APIError:
|
||||
_LOGGER.error("An error occured while setting the temperature")
|
||||
_LOGGER.error("An error occurred while setting the temperature")
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
@@ -187,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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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']
|
||||
@@ -75,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,
|
||||
}),
|
||||
@@ -110,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
|
||||
@@ -128,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]
|
||||
@@ -136,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):
|
||||
|
||||
@@ -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,9 +16,9 @@ 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)
|
||||
@@ -38,12 +38,11 @@ _CLOUD_ERRORS = {
|
||||
|
||||
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:
|
||||
@@ -57,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."""
|
||||
|
||||
@@ -68,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))
|
||||
|
||||
|
||||
@@ -91,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')
|
||||
|
||||
@@ -109,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]
|
||||
@@ -132,14 +153,13 @@ 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')
|
||||
@@ -155,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')
|
||||
@@ -178,14 +197,13 @@ 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')
|
||||
|
||||
@@ -14,49 +14,30 @@ from homeassistant.util.yaml import load_yaml, dump
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
|
||||
'entity_registry')
|
||||
'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."""
|
||||
@@ -66,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
|
||||
|
||||
|
||||
@@ -94,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:
|
||||
@@ -106,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)
|
||||
|
||||
@@ -129,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))
|
||||
@@ -141,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,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])
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('domain'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
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)
|
||||
|
||||
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
|
||||
@asyncio.coroutine
|
||||
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.flow.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."""
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -13,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__)
|
||||
|
||||
@@ -28,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({
|
||||
@@ -112,6 +123,25 @@ async def async_setup(hass, config):
|
||||
'[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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -181,6 +185,12 @@ async 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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@ from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, List, Sequence, Callable
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
@@ -19,7 +17,6 @@ from homeassistant.loader import bind_hass
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
@@ -28,7 +25,7 @@ from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.yaml import dump
|
||||
|
||||
@@ -76,7 +73,6 @@ ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_SOURCE_TYPE = 'source_type'
|
||||
ATTR_VENDOR = 'vendor'
|
||||
ATTR_CONSIDER_HOME = 'consider_home'
|
||||
|
||||
SOURCE_TYPE_GPS = 'gps'
|
||||
@@ -111,6 +107,9 @@ SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(vol.All(
|
||||
ATTR_ATTRIBUTES: dict,
|
||||
ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES),
|
||||
ATTR_CONSIDER_HOME: cv.time_period,
|
||||
# Temp workaround for iOS app introduced in 0.65
|
||||
vol.Optional('battery_status'): str,
|
||||
vol.Optional('hostname'): str,
|
||||
}))
|
||||
|
||||
|
||||
@@ -219,7 +218,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
@asyncio.coroutine
|
||||
def async_see_service(call):
|
||||
"""Service to see a device."""
|
||||
yield from tracker.async_see(**call.data)
|
||||
# Temp workaround for iOS, introduced in 0.65
|
||||
data = dict(call.data)
|
||||
data.pop('hostname', None)
|
||||
data.pop('battery_status', None)
|
||||
yield from tracker.async_see(**data)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA)
|
||||
@@ -321,14 +324,10 @@ class DeviceTracker(object):
|
||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
|
||||
|
||||
# lookup mac vendor string to be stored in config
|
||||
yield from device.set_vendor_for_mac()
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
ATTR_MAC: device.mac,
|
||||
ATTR_VENDOR: device.vendor,
|
||||
})
|
||||
|
||||
# update known_devices.yaml
|
||||
@@ -406,7 +405,6 @@ class Device(Entity):
|
||||
consider_home = None # type: dt_util.dt.timedelta
|
||||
battery = None # type: int
|
||||
attributes = None # type: dict
|
||||
vendor = None # type: str
|
||||
icon = None # type: str
|
||||
|
||||
# Track if the last update of this device was HOME.
|
||||
@@ -416,7 +414,7 @@ class Device(Entity):
|
||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track: bool, dev_id: str, mac: str, name: str = None,
|
||||
picture: str = None, gravatar: str = None, icon: str = None,
|
||||
hide_if_away: bool = False, vendor: str = None) -> None:
|
||||
hide_if_away: bool = False) -> None:
|
||||
"""Initialize a device."""
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
@@ -444,7 +442,6 @@ class Device(Entity):
|
||||
self.icon = icon
|
||||
|
||||
self.away_hide = hide_if_away
|
||||
self.vendor = vendor
|
||||
|
||||
self.source_type = None
|
||||
|
||||
@@ -560,51 +557,6 @@ class Device(Entity):
|
||||
self._state = STATE_HOME
|
||||
self.last_update_home = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def set_vendor_for_mac(self):
|
||||
"""Set vendor string using api.macvendors.com."""
|
||||
self.vendor = yield from self.get_vendor_for_mac()
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_vendor_for_mac(self):
|
||||
"""Try to find the vendor string for a given MAC address."""
|
||||
if not self.mac:
|
||||
return None
|
||||
|
||||
if '_' in self.mac:
|
||||
_, mac = self.mac.split('_', 1)
|
||||
else:
|
||||
mac = self.mac
|
||||
|
||||
if not len(mac.split(':')) == 6:
|
||||
return 'unknown'
|
||||
|
||||
# We only need the first 3 bytes of the MAC for a lookup
|
||||
# this improves somewhat on privacy
|
||||
oui_bytes = mac.split(':')[0:3]
|
||||
# bytes like 00 get truncates to 0, API needs full bytes
|
||||
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
||||
url = 'http://api.macvendors.com/' + oui
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
|
||||
with async_timeout.timeout(5, loop=self.hass.loop):
|
||||
resp = yield from websession.get(url)
|
||||
# mac vendor found, response is the string
|
||||
if resp.status == 200:
|
||||
vendor_string = yield from resp.text()
|
||||
return vendor_string
|
||||
# If vendor is not known to the API (404) or there
|
||||
# was a failure during the lookup (500); set vendor
|
||||
# to something other then None to prevent retry
|
||||
# as the value is only relevant when it is to be stored
|
||||
# in the 'known_devices.yaml' file which only happens
|
||||
# the first time the device is seen.
|
||||
return 'unknown'
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
# Same as above
|
||||
return 'unknown'
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add an entity."""
|
||||
@@ -653,6 +605,17 @@ class DeviceScanner(object):
|
||||
"""
|
||||
return self.hass.async_add_job(self.get_device_name, device)
|
||||
|
||||
def get_extra_attributes(self, device: str) -> dict:
|
||||
"""Get the extra attributes of a device."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_get_extra_attributes(self, device: str) -> Any:
|
||||
"""Get the extra attributes of a device.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.get_extra_attributes, device)
|
||||
|
||||
|
||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file."""
|
||||
@@ -678,7 +641,6 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
||||
vol.Optional('picture', default=None): vol.Any(None, cv.string),
|
||||
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional('vendor', default=None): vol.Any(None, cv.string),
|
||||
})
|
||||
try:
|
||||
result = []
|
||||
@@ -690,6 +652,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
||||
return []
|
||||
|
||||
for dev_id, device in devices.items():
|
||||
# Deprecated option. We just ignore it to avoid breaking change
|
||||
device.pop('vendor', None)
|
||||
try:
|
||||
device = dev_schema(device)
|
||||
device['dev_id'] = cv.slugify(dev_id)
|
||||
@@ -737,10 +701,20 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
host_name = yield from scanner.async_get_device_name(mac)
|
||||
seen.add(mac)
|
||||
|
||||
try:
|
||||
extra_attributes = (yield from
|
||||
scanner.async_get_extra_attributes(mac))
|
||||
except NotImplementedError:
|
||||
extra_attributes = dict()
|
||||
|
||||
kwargs = {
|
||||
'mac': mac,
|
||||
'host_name': host_name,
|
||||
'source_type': SOURCE_TYPE_ROUTER
|
||||
'source_type': SOURCE_TYPE_ROUTER,
|
||||
'attributes': {
|
||||
'scanner': scanner.__class__.__name__,
|
||||
**extra_attributes
|
||||
}
|
||||
}
|
||||
|
||||
zone_home = hass.states.get(zone.ENTITY_ID_HOME)
|
||||
@@ -765,7 +739,6 @@ def update_config(path: str, dev_id: str, device: Device):
|
||||
'picture': device.config_picture,
|
||||
'track': device.track,
|
||||
CONF_AWAY_HIDE: device.away_hide,
|
||||
'vendor': device.vendor,
|
||||
}}
|
||||
out.write('\n')
|
||||
out.write(dump(device))
|
||||
|
||||
@@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PUB_KEY = 'pub_key'
|
||||
CONF_SSH_KEY = 'ssh_key'
|
||||
CONF_REQUIRE_IP = 'require_ip'
|
||||
DEFAULT_SSH_PORT = 22
|
||||
SECRET_GROUP = 'Password or SSH Key'
|
||||
|
||||
@@ -36,6 +37,7 @@ PLATFORM_SCHEMA = vol.All(
|
||||
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
|
||||
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
|
||||
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
|
||||
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
|
||||
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
|
||||
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
|
||||
@@ -115,6 +117,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
self.protocol = config[CONF_PROTOCOL]
|
||||
self.mode = config[CONF_MODE]
|
||||
self.port = config[CONF_PORT]
|
||||
self.require_ip = config[CONF_REQUIRE_IP]
|
||||
|
||||
if self.protocol == 'ssh':
|
||||
self.connection = SshConnection(
|
||||
@@ -172,7 +175,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
|
||||
ret_devices = {}
|
||||
for key in devices:
|
||||
if devices[key].ip is not None:
|
||||
if not self.require_ip or devices[key].ip is not None:
|
||||
ret_devices[key] = devices[key]
|
||||
return ret_devices
|
||||
|
||||
|
||||
@@ -17,12 +17,15 @@ import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pybluez==0.22']
|
||||
REQUIREMENTS = ['pybluez==0.22', 'bt_proximity==0.1.2']
|
||||
|
||||
BT_PREFIX = 'BT_'
|
||||
|
||||
CONF_REQUEST_RSSI = 'request_rssi'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_TRACK_NEW): cv.boolean
|
||||
vol.Optional(CONF_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_REQUEST_RSSI): cv.boolean
|
||||
})
|
||||
|
||||
|
||||
@@ -30,11 +33,15 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up the Bluetooth Scanner."""
|
||||
# pylint: disable=import-error
|
||||
import bluetooth
|
||||
from bt_proximity import BluetoothRSSI
|
||||
|
||||
def see_device(device):
|
||||
def see_device(mac, name, rssi=None):
|
||||
"""Mark a device as seen."""
|
||||
see(mac=BT_PREFIX + device[0], host_name=device[1],
|
||||
source_type=SOURCE_TYPE_BLUETOOTH)
|
||||
attributes = {}
|
||||
if rssi is not None:
|
||||
attributes['rssi'] = rssi
|
||||
see(mac="{}{}".format(BT_PREFIX, mac), host_name=name,
|
||||
attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH)
|
||||
|
||||
def discover_devices():
|
||||
"""Discover Bluetooth devices."""
|
||||
@@ -64,27 +71,32 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
if track_new:
|
||||
for dev in discover_devices():
|
||||
if dev[0] not in devs_to_track and \
|
||||
dev[0] not in devs_donot_track:
|
||||
dev[0] not in devs_donot_track:
|
||||
devs_to_track.append(dev[0])
|
||||
see_device(dev)
|
||||
see_device(dev[0], dev[1])
|
||||
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
request_rssi = config.get(CONF_REQUEST_RSSI, False)
|
||||
|
||||
def update_bluetooth(now):
|
||||
"""Lookup Bluetooth device and update status."""
|
||||
try:
|
||||
if track_new:
|
||||
for dev in discover_devices():
|
||||
if dev[0] not in devs_to_track and \
|
||||
dev[0] not in devs_donot_track:
|
||||
dev[0] not in devs_donot_track:
|
||||
devs_to_track.append(dev[0])
|
||||
for mac in devs_to_track:
|
||||
_LOGGER.debug("Scanning %s", mac)
|
||||
result = bluetooth.lookup_name(mac, timeout=5)
|
||||
if not result:
|
||||
rssi = None
|
||||
if request_rssi:
|
||||
rssi = BluetoothRSSI(mac).request_rssi()
|
||||
if result is None:
|
||||
# Could not lookup device name
|
||||
continue
|
||||
see_device((mac, result))
|
||||
see_device(mac, result, rssi)
|
||||
except bluetooth.BluetoothError:
|
||||
_LOGGER.exception("Error looking up Bluetooth device")
|
||||
track_point_in_utc_time(
|
||||
|
||||
@@ -36,16 +36,23 @@ class BMWDeviceTracker(object):
|
||||
self.vehicle = vehicle
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the device info."""
|
||||
dev_id = slugify(self.vehicle.modelName)
|
||||
"""Update the device info.
|
||||
|
||||
Only update the state in home assistant if tracking in
|
||||
the car is enabled.
|
||||
"""
|
||||
dev_id = slugify(self.vehicle.name)
|
||||
|
||||
if not self.vehicle.state.is_vehicle_tracking_enabled:
|
||||
_LOGGER.debug('Tracking is disabled for vehicle %s', dev_id)
|
||||
return
|
||||
|
||||
_LOGGER.debug('Updating %s', dev_id)
|
||||
attrs = {
|
||||
'trackr_id': dev_id,
|
||||
'id': dev_id,
|
||||
'name': self.vehicle.modelName
|
||||
'vin': self.vehicle.vin,
|
||||
}
|
||||
self._see(
|
||||
dev_id=dev_id, host_name=self.vehicle.modelName,
|
||||
dev_id=dev_id, host_name=self.vehicle.name,
|
||||
gps=self.vehicle.state.gps_position, attributes=attrs,
|
||||
icon='mdi:car'
|
||||
)
|
||||
|
||||
83
homeassistant/components/device_tracker/google_maps.py
Normal file
83
homeassistant/components/device_tracker/google_maps.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Support for Google Maps location sharing.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.google_maps/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, SOURCE_TYPE_GPS)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==1.2.1']
|
||||
|
||||
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: ConfigType, see, discovery_info=None):
|
||||
"""Set up the scanner."""
|
||||
scanner = GoogleMapsScanner(hass, config, see)
|
||||
return scanner.success_init
|
||||
|
||||
|
||||
class GoogleMapsScanner(object):
|
||||
"""Representation of an Google Maps location sharing account."""
|
||||
|
||||
def __init__(self, hass, config: ConfigType, see) -> None:
|
||||
"""Initialize the scanner."""
|
||||
from locationsharinglib import Service
|
||||
from locationsharinglib.locationsharinglibexceptions import InvalidUser
|
||||
|
||||
self.see = see
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
self.service = Service(self.username, self.password,
|
||||
hass.config.path(CREDENTIALS_FILE))
|
||||
self._update_info()
|
||||
|
||||
track_time_interval(
|
||||
hass, self._update_info, MIN_TIME_BETWEEN_SCANS)
|
||||
|
||||
self.success_init = True
|
||||
|
||||
except InvalidUser:
|
||||
_LOGGER.error('You have specified invalid login credentials')
|
||||
self.success_init = False
|
||||
|
||||
def _update_info(self, now=None):
|
||||
for person in self.service.get_all_people():
|
||||
dev_id = 'google_maps_{0}'.format(slugify(person.id))
|
||||
|
||||
attrs = {
|
||||
'id': person.id,
|
||||
'nickname': person.nickname,
|
||||
'full_name': person.full_name,
|
||||
'last_seen': person.datetime,
|
||||
'address': person.address
|
||||
}
|
||||
self.see(
|
||||
dev_id=dev_id,
|
||||
gps=(person.latitude, person.longitude),
|
||||
picture=person.picture_url,
|
||||
source_type=SOURCE_TYPE_GPS,
|
||||
attributes=attrs
|
||||
)
|
||||
@@ -1,74 +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/device_tracker.mercedesme/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.mercedesme import DATA_MME
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['mercedesme']
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up the Mercedes ME tracker."""
|
||||
if discovery_info is None:
|
||||
return False
|
||||
|
||||
data = hass.data[DATA_MME].data
|
||||
|
||||
if not data.cars:
|
||||
return False
|
||||
|
||||
MercedesMEDeviceTracker(hass, config, see, data)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MercedesMEDeviceTracker(object):
|
||||
"""A class representing a Mercedes ME device tracker."""
|
||||
|
||||
def __init__(self, hass, config, see, data):
|
||||
"""Initialize the Mercedes ME device tracker."""
|
||||
self.see = see
|
||||
self.data = data
|
||||
self.update_info()
|
||||
|
||||
track_time_interval(
|
||||
hass, self.update_info, MIN_TIME_BETWEEN_SCANS)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def update_info(self, now=None):
|
||||
"""Update the device info."""
|
||||
for device in self.data.cars:
|
||||
if not device['services'].get('VEHICLE_FINDER', False):
|
||||
continue
|
||||
|
||||
location = self.data.get_location(device["vin"])
|
||||
if location is None:
|
||||
continue
|
||||
|
||||
dev_id = device["vin"]
|
||||
name = device["license"]
|
||||
|
||||
lat = location['positionLat']['value']
|
||||
lon = location['positionLong']['value']
|
||||
attrs = {
|
||||
'trackr_id': dev_id,
|
||||
'id': dev_id,
|
||||
'name': name
|
||||
}
|
||||
self.see(
|
||||
dev_id=dev_id, host_name=name,
|
||||
gps=(lat, lon), attributes=attrs
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner):
|
||||
self.host,
|
||||
self.username,
|
||||
self.password,
|
||||
port=int(self.port)
|
||||
port=int(self.port),
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -175,7 +176,7 @@ class MikrotikScanner(DeviceScanner):
|
||||
for device in device_names
|
||||
if device.get('mac-address')}
|
||||
|
||||
if self.wireless_exist:
|
||||
if self.wireless_exist or self.capsman_exist:
|
||||
self.last_results = {
|
||||
device.get('mac-address'):
|
||||
mac_names.get(device.get('mac-address'))
|
||||
|
||||
@@ -6,15 +6,15 @@ https://home-assistant.io/components/device_tracker.mysensors/
|
||||
"""
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util import slugify
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up the MySensors device scanner."""
|
||||
new_devices = mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
|
||||
device_args=(see, ))
|
||||
device_args=(async_see, ))
|
||||
if not new_devices:
|
||||
return False
|
||||
|
||||
@@ -22,9 +22,9 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
dev_id = (
|
||||
id(device.gateway), device.node_id, device.child_id,
|
||||
device.value_type)
|
||||
dispatcher_connect(
|
||||
async_dispatcher_connect(
|
||||
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
|
||||
device.update_callback)
|
||||
device.async_update_callback)
|
||||
|
||||
return True
|
||||
|
||||
@@ -32,20 +32,20 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
class MySensorsDeviceScanner(mysensors.MySensorsDevice):
|
||||
"""Represent a MySensors scanner."""
|
||||
|
||||
def __init__(self, see, *args):
|
||||
def __init__(self, async_see, *args):
|
||||
"""Set up instance."""
|
||||
super().__init__(*args)
|
||||
self.see = see
|
||||
self.async_see = async_see
|
||||
|
||||
def update_callback(self):
|
||||
async def async_update_callback(self):
|
||||
"""Update the device."""
|
||||
self.update()
|
||||
await self.async_update()
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
child = node.children[self.child_id]
|
||||
position = child.values[self.value_type]
|
||||
latitude, longitude, _ = position.split(',')
|
||||
|
||||
self.see(
|
||||
await self.async_see(
|
||||
dev_id=slugify(self.name),
|
||||
host_name=self.name,
|
||||
gps=(latitude, longitude),
|
||||
|
||||
@@ -80,6 +80,8 @@ class NmapDeviceScanner(DeviceScanner):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
_LOGGER.debug("Nmap last results %s", self.last_results)
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
@@ -91,6 +93,13 @@ class NmapDeviceScanner(DeviceScanner):
|
||||
return filter_named[0]
|
||||
return None
|
||||
|
||||
def get_extra_attributes(self, device):
|
||||
"""Return the IP of the given device."""
|
||||
filter_ip = next((
|
||||
result.ip for result in self.last_results
|
||||
if result.mac == device), None)
|
||||
return {'ip': filter_ip}
|
||||
|
||||
def _update_info(self):
|
||||
"""Scan the network for devices.
|
||||
|
||||
|
||||
@@ -68,22 +68,18 @@ class TadoDeviceScanner(DeviceScanner):
|
||||
self.websession = async_create_clientsession(
|
||||
hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop))
|
||||
|
||||
self.success_init = self._update_info()
|
||||
self.success_init = asyncio.run_coroutine_threadsafe(
|
||||
self._async_update_info(), hass.loop
|
||||
).result()
|
||||
|
||||
_LOGGER.info("Scanner initialized")
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_scan_devices(self):
|
||||
async def async_scan_devices(self):
|
||||
"""Scan for devices and return a list containing found device ids."""
|
||||
info = self._update_info()
|
||||
|
||||
# Don't yield if we got None
|
||||
if info is not None:
|
||||
yield from info
|
||||
|
||||
await self._async_update_info()
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_device_name(self, device):
|
||||
async def async_get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
filter_named = [result.name for result in self.last_results
|
||||
if result.mac == device]
|
||||
@@ -93,7 +89,7 @@ class TadoDeviceScanner(DeviceScanner):
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
async def _async_update_info(self):
|
||||
"""
|
||||
Query Tado for device marked as at home.
|
||||
|
||||
@@ -104,21 +100,21 @@ class TadoDeviceScanner(DeviceScanner):
|
||||
last_results = []
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
with async_timeout.timeout(10):
|
||||
# Format the URL here, so we can log the template URL if
|
||||
# anything goes wrong without exposing username and password.
|
||||
url = self.tadoapiurl.format(
|
||||
home_id=self.home_id, username=self.username,
|
||||
password=self.password)
|
||||
|
||||
response = yield from self.websession.get(url)
|
||||
response = await self.websession.get(url)
|
||||
|
||||
if response.status != 200:
|
||||
_LOGGER.warning(
|
||||
"Error %d on %s.", response.status, self.tadoapiurl)
|
||||
return
|
||||
return False
|
||||
|
||||
tado_json = yield from response.json()
|
||||
tado_json = await response.json()
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Cannot load Tado data")
|
||||
@@ -139,7 +135,7 @@ class TadoDeviceScanner(DeviceScanner):
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info(
|
||||
_LOGGER.debug(
|
||||
"Tado presence query successful, %d device(s) at home",
|
||||
len(self.last_results)
|
||||
)
|
||||
|
||||
@@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
return self.last_results
|
||||
|
||||
def _generate_mac2name(self):
|
||||
"""Return empty MAC to name dict. Overriden if DHCP server is set."""
|
||||
"""Return empty MAC to name dict. Overridden if DHCP server is set."""
|
||||
self.mac2name = dict()
|
||||
|
||||
@_refresh_on_access_denied
|
||||
@@ -103,6 +103,9 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if self.mac2name is None:
|
||||
self._generate_mac2name()
|
||||
if self.mac2name is None:
|
||||
# Generation of mac2name dictionary failed
|
||||
return None
|
||||
name = self.mac2name.get(device.upper(), None)
|
||||
return name
|
||||
|
||||
|
||||
@@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner):
|
||||
# Filter clients to provided SSID list
|
||||
if self._ssid_filter:
|
||||
clients = [client for client in clients
|
||||
if client['essid'] in self._ssid_filter]
|
||||
if 'essid' in client and
|
||||
client['essid'] in self._ssid_filter]
|
||||
|
||||
self._clients = {
|
||||
client['mac']: client
|
||||
@@ -121,3 +122,9 @@ class UnifiScanner(DeviceScanner):
|
||||
name = client.get('name') or client.get('hostname')
|
||||
_LOGGER.debug("Device mac %s name %s", device, name)
|
||||
return name
|
||||
|
||||
def get_extra_attributes(self, device):
|
||||
"""Return the extra attributes of the device."""
|
||||
client = self._clients.get(device, {})
|
||||
_LOGGER.debug("Device mac %s attributes %s", device, client)
|
||||
return client
|
||||
|
||||
77
homeassistant/components/device_tracker/xiaomi_miio.py
Normal file
77
homeassistant/components/device_tracker/xiaomi_miio.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Support for Xiaomi Mi WiFi Repeater 2.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/device_tracker.xiaomi_miio/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
|
||||
DeviceScanner)
|
||||
from homeassistant.const import (CONF_HOST, CONF_TOKEN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Return a Xiaomi MiIO device scanner."""
|
||||
from miio import WifiRepeater, DeviceException
|
||||
|
||||
scanner = None
|
||||
host = config[DOMAIN].get(CONF_HOST)
|
||||
token = config[DOMAIN].get(CONF_TOKEN)
|
||||
|
||||
_LOGGER.info(
|
||||
"Initializing with host %s (token %s...)", host, token[:5])
|
||||
|
||||
try:
|
||||
device = WifiRepeater(host, token)
|
||||
device_info = device.info()
|
||||
_LOGGER.info("%s %s %s detected",
|
||||
device_info.model,
|
||||
device_info.firmware_version,
|
||||
device_info.hardware_version)
|
||||
scanner = XiaomiMiioDeviceScanner(device)
|
||||
except DeviceException as ex:
|
||||
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
|
||||
|
||||
return scanner
|
||||
|
||||
|
||||
class XiaomiMiioDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Xiaomi Mi WiFi Repeater."""
|
||||
|
||||
def __init__(self, device):
|
||||
"""Initialize the scanner."""
|
||||
self.device = device
|
||||
|
||||
async def async_scan_devices(self):
|
||||
"""Scan for devices and return a list containing found device ids."""
|
||||
from miio import DeviceException
|
||||
|
||||
devices = []
|
||||
try:
|
||||
station_info = await self.hass.async_add_job(self.device.status)
|
||||
_LOGGER.debug("Got new station info: %s", station_info)
|
||||
|
||||
for device in station_info['mat']:
|
||||
devices.append(device['mac'])
|
||||
|
||||
except DeviceException as ex:
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return devices
|
||||
|
||||
async def async_get_device_name(self, device):
|
||||
"""The repeater doesn't provide the name of the associated device."""
|
||||
return None
|
||||
@@ -6,7 +6,6 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
|
||||
Knows which components handle certain types, will make sure they are
|
||||
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
@@ -14,6 +13,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==1.2.4']
|
||||
REQUIREMENTS = ['netdisco==1.3.1']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
@@ -39,6 +39,13 @@ SERVICE_TELLDUSLIVE = 'tellstick'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_DECONZ = 'deconz'
|
||||
SERVICE_DAIKIN = 'daikin'
|
||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||
SERVICE_HOMEKIT = 'homekit'
|
||||
|
||||
CONFIG_ENTRY_HANDLERS = {
|
||||
SERVICE_DECONZ: 'deconz',
|
||||
SERVICE_HUE: 'hue',
|
||||
}
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
@@ -51,9 +58,8 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_WINK: ('wink', None),
|
||||
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
||||
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
||||
SERVICE_HUE: ('hue', None),
|
||||
SERVICE_DECONZ: ('deconz', None),
|
||||
SERVICE_DAIKIN: ('daikin', None),
|
||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
@@ -72,20 +78,28 @@ SERVICE_HANDLERS = {
|
||||
'bose_soundtouch': ('media_player', 'soundtouch'),
|
||||
'bluesound': ('media_player', 'bluesound'),
|
||||
'songpal': ('media_player', 'songpal'),
|
||||
'kodi': ('media_player', 'kodi'),
|
||||
}
|
||||
|
||||
OPTIONAL_SERVICE_HANDLERS = {
|
||||
SERVICE_HOMEKIT: ('homekit_controller', None),
|
||||
}
|
||||
|
||||
CONF_IGNORE = 'ignore'
|
||||
CONF_ENABLE = 'enable'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(DOMAIN): vol.Schema({
|
||||
vol.Optional(CONF_IGNORE, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(SERVICE_HANDLERS)])
|
||||
vol.All(cv.ensure_list, [
|
||||
vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]),
|
||||
vol.Optional(CONF_ENABLE, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(OPTIONAL_SERVICE_HANDLERS)])
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Start a discovery service."""
|
||||
from netdisco.discovery import NetworkDiscovery
|
||||
|
||||
@@ -99,40 +113,52 @@ def async_setup(hass, config):
|
||||
# Platforms ignore by config
|
||||
ignored_platforms = config[DOMAIN][CONF_IGNORE]
|
||||
|
||||
@asyncio.coroutine
|
||||
def new_service_found(service, info):
|
||||
# Optional platforms enabled by config
|
||||
enabled_platforms = config[DOMAIN][CONF_ENABLE]
|
||||
|
||||
async def new_service_found(service, info):
|
||||
"""Handle a new service if one is found."""
|
||||
if service in ignored_platforms:
|
||||
logger.info("Ignoring service: %s %s", service, info)
|
||||
return
|
||||
|
||||
comp_plat = SERVICE_HANDLERS.get(service)
|
||||
|
||||
# We do not know how to handle this service.
|
||||
if not comp_plat:
|
||||
logger.info("Unknown service discovered: %s %s", service, info)
|
||||
return
|
||||
|
||||
discovery_hash = json.dumps([service, info], sort_keys=True)
|
||||
if discovery_hash in already_discovered:
|
||||
return
|
||||
|
||||
already_discovered.add(discovery_hash)
|
||||
|
||||
if service in CONFIG_ENTRY_HANDLERS:
|
||||
await hass.config_entries.flow.async_init(
|
||||
CONFIG_ENTRY_HANDLERS[service],
|
||||
source=data_entry_flow.SOURCE_DISCOVERY,
|
||||
data=info
|
||||
)
|
||||
return
|
||||
|
||||
comp_plat = SERVICE_HANDLERS.get(service)
|
||||
|
||||
if not comp_plat and service in enabled_platforms:
|
||||
comp_plat = OPTIONAL_SERVICE_HANDLERS[service]
|
||||
|
||||
# We do not know how to handle this service.
|
||||
if not comp_plat:
|
||||
logger.info("Unknown service discovered: %s %s", service, info)
|
||||
return
|
||||
|
||||
logger.info("Found new service: %s %s", service, info)
|
||||
|
||||
component, platform = comp_plat
|
||||
|
||||
if platform is None:
|
||||
yield from async_discover(hass, service, info, component, config)
|
||||
await async_discover(hass, service, info, component, config)
|
||||
else:
|
||||
yield from async_load_platform(
|
||||
await async_load_platform(
|
||||
hass, component, platform, info, config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def scan_devices(now):
|
||||
async def scan_devices(now):
|
||||
"""Scan for devices."""
|
||||
results = yield from hass.async_add_job(_discover, netdisco)
|
||||
results = await hass.async_add_job(_discover, netdisco)
|
||||
|
||||
for result in results:
|
||||
hass.async_add_job(new_service_found(*result))
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['DoorBirdPy==0.1.2']
|
||||
REQUIREMENTS = ['DoorBirdPy==0.1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,6 +22,7 @@ DOMAIN = 'doorbird'
|
||||
API_URL = '/api/{}'.format(DOMAIN)
|
||||
|
||||
CONF_DOORBELL_EVENTS = 'doorbell_events'
|
||||
CONF_CUSTOM_URL = 'hass_url_override'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
|
||||
vol.Optional(CONF_CUSTOM_URL): cv.string,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -61,9 +63,17 @@ def setup(hass, config):
|
||||
# Provide an endpoint for the device to call to trigger events
|
||||
hass.http.register_view(DoorbirdRequestView())
|
||||
|
||||
# Get the URL of this server
|
||||
hass_url = hass.config.api.base_url
|
||||
|
||||
# Override it if another is specified in the component configuration
|
||||
if config[DOMAIN].get(CONF_CUSTOM_URL):
|
||||
hass_url = config[DOMAIN].get(CONF_CUSTOM_URL)
|
||||
_LOGGER.info("DoorBird will connect to this instance via %s",
|
||||
hass_url)
|
||||
|
||||
# This will make HA the only service that gets doorbell events
|
||||
url = '{}{}/{}'.format(
|
||||
hass.config.api.base_url, API_URL, SENSOR_DOORBELL)
|
||||
url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
|
||||
device.reset_notifications()
|
||||
device.subscribe_notification(SENSOR_DOORBELL, url)
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ ATTR_OVERWRITE = 'overwrite'
|
||||
CONF_DOWNLOAD_DIR = 'download_dir'
|
||||
|
||||
DOMAIN = 'downloader'
|
||||
DOWNLOAD_FAILED_EVENT = 'download_failed'
|
||||
DOWNLOAD_COMPLETED_EVENT = 'download_completed'
|
||||
|
||||
SERVICE_DOWNLOAD_FILE = 'download_file'
|
||||
|
||||
@@ -133,9 +135,19 @@ def setup(hass, config):
|
||||
fil.write(chunk)
|
||||
|
||||
_LOGGER.debug("Downloading of %s done", url)
|
||||
hass.bus.fire(
|
||||
"{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), {
|
||||
'url': url,
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.exception("ConnectionError occurred for %s", url)
|
||||
hass.bus.fire(
|
||||
"{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), {
|
||||
'url': url,
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
# Remove file if we started downloading but failed
|
||||
if final_path and os.path.isfile(final_path):
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.util.json import save_json
|
||||
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.15']
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.18']
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.38']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.39']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -158,10 +158,6 @@ class Config(object):
|
||||
"Listen port not specified, defaulting to %s",
|
||||
self.listen_port)
|
||||
|
||||
if self.type == TYPE_GOOGLE and self.listen_port != 80:
|
||||
_LOGGER.warning("When targeting Google Home, listening port has "
|
||||
"to be port 80")
|
||||
|
||||
# Get whether or not UPNP binds to multicast address (239.255.255.250)
|
||||
# or to the unicast address (host_ip_addr)
|
||||
self.upnp_bind_multicast = conf.get(
|
||||
|
||||
77
homeassistant/components/eufy.py
Normal file
77
homeassistant/components/eufy.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Support for Eufy devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/eufy/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, \
|
||||
CONF_DEVICES, CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, CONF_NAME
|
||||
from homeassistant.helpers import discovery
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
REQUIREMENTS = ['lakeside==0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'eufy'
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ADDRESS): cv.string,
|
||||
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
||||
vol.Required(CONF_TYPE): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list,
|
||||
[DEVICE_SCHEMA]),
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
EUFY_DISPATCH = {
|
||||
'T1011': 'light',
|
||||
'T1012': 'light',
|
||||
'T1013': 'light',
|
||||
'T1201': 'switch',
|
||||
'T1202': 'switch',
|
||||
'T1211': 'switch'
|
||||
}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up Eufy devices."""
|
||||
# pylint: disable=import-error
|
||||
import lakeside
|
||||
|
||||
if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]:
|
||||
data = lakeside.get_devices(config[DOMAIN][CONF_USERNAME],
|
||||
config[DOMAIN][CONF_PASSWORD])
|
||||
for device in data:
|
||||
kind = device['type']
|
||||
if kind not in EUFY_DISPATCH:
|
||||
continue
|
||||
discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device,
|
||||
config)
|
||||
|
||||
for device_info in config[DOMAIN][CONF_DEVICES]:
|
||||
kind = device_info['type']
|
||||
if kind not in EUFY_DISPATCH:
|
||||
continue
|
||||
device = {}
|
||||
device['address'] = device_info['address']
|
||||
device['code'] = device_info['access_token']
|
||||
device['type'] = device_info['type']
|
||||
device['name'] = device_info['name']
|
||||
discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device,
|
||||
config)
|
||||
|
||||
return True
|
||||
@@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on:
|
||||
description: Turn the buzzer on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_buzzer_off:
|
||||
description: Turn the buzzer off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_led_on:
|
||||
description: Turn the led on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_led_off:
|
||||
description: Turn the led off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_child_lock_on:
|
||||
description: Turn the child lock on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_child_lock_off:
|
||||
description: Turn the child lock off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_favorite_level:
|
||||
description: Set the favorite level.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
level:
|
||||
description: Level, between 0 and 16.
|
||||
example: 1
|
||||
@@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness:
|
||||
description: Set the led brightness.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
brightness:
|
||||
description: Brightness (0 = Bright, 1 = Dim, 2 = Off)
|
||||
example: 1
|
||||
|
||||
xiaomi_miio_set_auto_detect_on:
|
||||
description: Turn the auto detect on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_auto_detect_off:
|
||||
description: Turn the auto detect off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_learn_mode_on:
|
||||
description: Turn the learn mode on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_learn_mode_off:
|
||||
description: Turn the learn mode off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_volume:
|
||||
description: Set the sound volume.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
volume:
|
||||
description: Volume, between 0 and 100.
|
||||
example: 50
|
||||
|
||||
xiaomi_miio_reset_filter:
|
||||
description: Reset the filter lifetime and usage.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_extra_features:
|
||||
description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
features:
|
||||
description: Integer, known values are 0 (default) and 1 (turbo mode).
|
||||
example: 1
|
||||
|
||||
xiaomi_miio_set_target_humidity:
|
||||
description: Set the target humidity.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
humidity:
|
||||
description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80.
|
||||
example: 50
|
||||
|
||||
xiaomi_miio_set_dry_on:
|
||||
description: Turn the dry mode on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_dry_off:
|
||||
description: Turn the dry mode off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""
|
||||
Support for Xiaomi Mi Air Purifier 2.
|
||||
Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/fan.xiaomi_miio/
|
||||
"""
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
|
||||
SUPPORT_SET_SPEED, DOMAIN, )
|
||||
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
|
||||
@@ -20,17 +20,40 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Xiaomi Air Purifier'
|
||||
PLATFORM = 'xiaomi_miio'
|
||||
DEFAULT_NAME = 'Xiaomi Miio Device'
|
||||
DATA_KEY = 'fan.xiaomi_miio'
|
||||
|
||||
CONF_MODEL = 'model'
|
||||
MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6'
|
||||
MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3'
|
||||
MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1'
|
||||
MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_MODEL): vol.In(
|
||||
['zhimi.airpurifier.m1',
|
||||
'zhimi.airpurifier.m2',
|
||||
'zhimi.airpurifier.ma1',
|
||||
'zhimi.airpurifier.ma2',
|
||||
'zhimi.airpurifier.sa1',
|
||||
'zhimi.airpurifier.sa2',
|
||||
'zhimi.airpurifier.v1',
|
||||
'zhimi.airpurifier.v2',
|
||||
'zhimi.airpurifier.v3',
|
||||
'zhimi.airpurifier.v5',
|
||||
'zhimi.airpurifier.v6',
|
||||
'zhimi.humidifier.v1',
|
||||
'zhimi.humidifier.ca1']),
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.7']
|
||||
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
|
||||
|
||||
ATTR_MODEL = 'model'
|
||||
|
||||
# Air Purifier
|
||||
ATTR_TEMPERATURE = 'temperature'
|
||||
ATTR_HUMIDITY = 'humidity'
|
||||
ATTR_AIR_QUALITY_INDEX = 'aqi'
|
||||
@@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness'
|
||||
ATTR_MOTOR_SPEED = 'motor_speed'
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi'
|
||||
ATTR_PURIFY_VOLUME = 'purify_volume'
|
||||
|
||||
ATTR_BRIGHTNESS = 'brightness'
|
||||
ATTR_LEVEL = 'level'
|
||||
ATTR_MOTOR2_SPEED = 'motor2_speed'
|
||||
ATTR_ILLUMINANCE = 'illuminance'
|
||||
ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id'
|
||||
ATTR_FILTER_RFID_TAG = 'filter_rfid_tag'
|
||||
ATTR_FILTER_TYPE = 'filter_type'
|
||||
ATTR_LEARN_MODE = 'learn_mode'
|
||||
ATTR_SLEEP_TIME = 'sleep_time'
|
||||
ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count'
|
||||
ATTR_EXTRA_FEATURES = 'extra_features'
|
||||
ATTR_FEATURES = 'features'
|
||||
ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported'
|
||||
ATTR_AUTO_DETECT = 'auto_detect'
|
||||
ATTR_SLEEP_MODE = 'sleep_mode'
|
||||
ATTR_VOLUME = 'volume'
|
||||
ATTR_USE_TIME = 'use_time'
|
||||
ATTR_BUTTON_PRESSED = 'button_pressed'
|
||||
|
||||
# Air Humidifier
|
||||
ATTR_TARGET_HUMIDITY = 'target_humidity'
|
||||
ATTR_TRANS_LEVEL = 'trans_level'
|
||||
ATTR_HARDWARE_VERSION = 'hardware_version'
|
||||
|
||||
# Air Humidifier CA
|
||||
ATTR_SPEED = 'speed'
|
||||
ATTR_DEPTH = 'depth'
|
||||
ATTR_DRY = 'dry'
|
||||
|
||||
# Map attributes to properties of the state object
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
|
||||
ATTR_TEMPERATURE: 'temperature',
|
||||
ATTR_HUMIDITY: 'humidity',
|
||||
ATTR_AIR_QUALITY_INDEX: 'aqi',
|
||||
ATTR_MODE: 'mode',
|
||||
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
|
||||
ATTR_FILTER_LIFE: 'filter_life_remaining',
|
||||
ATTR_FAVORITE_LEVEL: 'favorite_level',
|
||||
ATTR_CHILD_LOCK: 'child_lock',
|
||||
ATTR_LED: 'led',
|
||||
ATTR_MOTOR_SPEED: 'motor_speed',
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
|
||||
ATTR_PURIFY_VOLUME: 'purify_volume',
|
||||
ATTR_LEARN_MODE: 'learn_mode',
|
||||
ATTR_SLEEP_TIME: 'sleep_time',
|
||||
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
|
||||
ATTR_EXTRA_FEATURES: 'extra_features',
|
||||
ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported',
|
||||
ATTR_AUTO_DETECT: 'auto_detect',
|
||||
ATTR_USE_TIME: 'use_time',
|
||||
ATTR_BUTTON_PRESSED: 'button_pressed',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER = {
|
||||
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
||||
ATTR_BUZZER: 'buzzer',
|
||||
ATTR_LED_BRIGHTNESS: 'led_brightness',
|
||||
ATTR_SLEEP_MODE: 'sleep_mode',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = {
|
||||
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
||||
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
|
||||
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
|
||||
ATTR_FILTER_TYPE: 'filter_type',
|
||||
ATTR_ILLUMINANCE: 'illuminance',
|
||||
ATTR_MOTOR2_SPEED: 'motor2_speed',
|
||||
ATTR_VOLUME: 'volume',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
|
||||
# Common set isn't used here. It's a very basic version of the device.
|
||||
ATTR_AIR_QUALITY_INDEX: 'aqi',
|
||||
ATTR_MODE: 'mode',
|
||||
ATTR_LED: 'led',
|
||||
ATTR_BUZZER: 'buzzer',
|
||||
ATTR_CHILD_LOCK: 'child_lock',
|
||||
ATTR_ILLUMINANCE: 'illuminance',
|
||||
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
|
||||
ATTR_FILTER_LIFE: 'filter_life_remaining',
|
||||
ATTR_MOTOR_SPEED: 'motor_speed',
|
||||
# perhaps supported but unconfirmed
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
|
||||
ATTR_VOLUME: 'volume',
|
||||
ATTR_MOTOR2_SPEED: 'motor2_speed',
|
||||
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
|
||||
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
|
||||
ATTR_FILTER_TYPE: 'filter_type',
|
||||
ATTR_PURIFY_VOLUME: 'purify_volume',
|
||||
ATTR_LEARN_MODE: 'learn_mode',
|
||||
ATTR_SLEEP_TIME: 'sleep_time',
|
||||
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
|
||||
ATTR_EXTRA_FEATURES: 'extra_features',
|
||||
ATTR_AUTO_DETECT: 'auto_detect',
|
||||
ATTR_USE_TIME: 'use_time',
|
||||
ATTR_BUTTON_PRESSED: 'button_pressed',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
|
||||
ATTR_TEMPERATURE: 'temperature',
|
||||
ATTR_HUMIDITY: 'humidity',
|
||||
ATTR_MODE: 'mode',
|
||||
ATTR_BUZZER: 'buzzer',
|
||||
ATTR_CHILD_LOCK: 'child_lock',
|
||||
ATTR_TRANS_LEVEL: 'trans_level',
|
||||
ATTR_TARGET_HUMIDITY: 'target_humidity',
|
||||
ATTR_LED_BRIGHTNESS: 'led_brightness',
|
||||
ATTR_BUTTON_PRESSED: 'button_pressed',
|
||||
ATTR_USE_TIME: 'use_time',
|
||||
ATTR_HARDWARE_VERSION: 'hardware_version',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = {
|
||||
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER,
|
||||
ATTR_SPEED: 'speed',
|
||||
ATTR_DEPTH: 'depth',
|
||||
ATTR_DRY: 'dry',
|
||||
}
|
||||
|
||||
OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
|
||||
OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
|
||||
OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
|
||||
'Medium', 'High', 'Strong']
|
||||
|
||||
SUCCESS = ['ok']
|
||||
|
||||
FEATURE_SET_BUZZER = 1
|
||||
FEATURE_SET_LED = 2
|
||||
FEATURE_SET_CHILD_LOCK = 4
|
||||
FEATURE_SET_LED_BRIGHTNESS = 8
|
||||
FEATURE_SET_FAVORITE_LEVEL = 16
|
||||
FEATURE_SET_AUTO_DETECT = 32
|
||||
FEATURE_SET_LEARN_MODE = 64
|
||||
FEATURE_SET_VOLUME = 128
|
||||
FEATURE_RESET_FILTER = 256
|
||||
FEATURE_SET_EXTRA_FEATURES = 512
|
||||
FEATURE_SET_TARGET_HUMIDITY = 1024
|
||||
FEATURE_SET_DRY = 2048
|
||||
|
||||
FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER |
|
||||
FEATURE_SET_CHILD_LOCK)
|
||||
|
||||
FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC |
|
||||
FEATURE_SET_LED |
|
||||
FEATURE_SET_LED_BRIGHTNESS |
|
||||
FEATURE_SET_FAVORITE_LEVEL |
|
||||
FEATURE_SET_LEARN_MODE |
|
||||
FEATURE_RESET_FILTER |
|
||||
FEATURE_SET_EXTRA_FEATURES)
|
||||
|
||||
FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK |
|
||||
FEATURE_SET_LED |
|
||||
FEATURE_SET_FAVORITE_LEVEL |
|
||||
FEATURE_SET_AUTO_DETECT |
|
||||
FEATURE_SET_VOLUME)
|
||||
|
||||
FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC |
|
||||
FEATURE_SET_LED)
|
||||
|
||||
FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC |
|
||||
FEATURE_SET_LED_BRIGHTNESS |
|
||||
FEATURE_SET_TARGET_HUMIDITY)
|
||||
|
||||
FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER |
|
||||
FEATURE_SET_DRY)
|
||||
|
||||
SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on'
|
||||
SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off'
|
||||
SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on'
|
||||
SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off'
|
||||
SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on'
|
||||
SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off'
|
||||
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
|
||||
SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness'
|
||||
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
|
||||
SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on'
|
||||
SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off'
|
||||
SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on'
|
||||
SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off'
|
||||
SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume'
|
||||
SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter'
|
||||
SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features'
|
||||
SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity'
|
||||
SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on'
|
||||
SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off'
|
||||
|
||||
AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
@@ -74,6 +267,21 @@ SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16))
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||
vol.Required(ATTR_VOLUME):
|
||||
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100))
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||
vol.Required(ATTR_FEATURES):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||
vol.Required(ATTR_HUMIDITY):
|
||||
vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80]))
|
||||
})
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'},
|
||||
SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'},
|
||||
@@ -81,59 +289,99 @@ SERVICE_TO_METHOD = {
|
||||
SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'},
|
||||
SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'},
|
||||
SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'},
|
||||
SERVICE_SET_FAVORITE_LEVEL: {
|
||||
'method': 'async_set_favorite_level',
|
||||
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
|
||||
SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'},
|
||||
SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'},
|
||||
SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'},
|
||||
SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'},
|
||||
SERVICE_RESET_FILTER: {'method': 'async_reset_filter'},
|
||||
SERVICE_SET_LED_BRIGHTNESS: {
|
||||
'method': 'async_set_led_brightness',
|
||||
'schema': SERVICE_SCHEMA_LED_BRIGHTNESS},
|
||||
SERVICE_SET_FAVORITE_LEVEL: {
|
||||
'method': 'async_set_favorite_level',
|
||||
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
|
||||
SERVICE_SET_VOLUME: {
|
||||
'method': 'async_set_volume',
|
||||
'schema': SERVICE_SCHEMA_VOLUME},
|
||||
SERVICE_SET_EXTRA_FEATURES: {
|
||||
'method': 'async_set_extra_features',
|
||||
'schema': SERVICE_SCHEMA_EXTRA_FEATURES},
|
||||
SERVICE_SET_TARGET_HUMIDITY: {
|
||||
'method': 'async_set_target_humidity',
|
||||
'schema': SERVICE_SCHEMA_TARGET_HUMIDITY},
|
||||
SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'},
|
||||
SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'},
|
||||
}
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the air purifier from config."""
|
||||
from miio import AirPurifier, DeviceException
|
||||
if PLATFORM not in hass.data:
|
||||
hass.data[PLATFORM] = {}
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the miio fan device from config."""
|
||||
from miio import Device, DeviceException
|
||||
if DATA_KEY not in hass.data:
|
||||
hass.data[DATA_KEY] = {}
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
token = config.get(CONF_TOKEN)
|
||||
model = config.get(CONF_MODEL)
|
||||
|
||||
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
|
||||
unique_id = None
|
||||
|
||||
try:
|
||||
if model is None:
|
||||
try:
|
||||
miio_device = Device(host, token)
|
||||
device_info = miio_device.info()
|
||||
model = device_info.model
|
||||
unique_id = "{}-{}".format(model, device_info.mac_address)
|
||||
_LOGGER.info("%s %s %s detected",
|
||||
model,
|
||||
device_info.firmware_version,
|
||||
device_info.hardware_version)
|
||||
except DeviceException:
|
||||
raise PlatformNotReady
|
||||
|
||||
if model.startswith('zhimi.airpurifier.'):
|
||||
from miio import AirPurifier
|
||||
air_purifier = AirPurifier(host, token)
|
||||
device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
|
||||
elif model.startswith('zhimi.humidifier.'):
|
||||
from miio import AirHumidifier
|
||||
air_humidifier = AirHumidifier(host, token)
|
||||
device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Unsupported device found! Please create an issue at '
|
||||
'https://github.com/syssi/xiaomi_airpurifier/issues '
|
||||
'and provide the following data: %s', model)
|
||||
return False
|
||||
|
||||
xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier)
|
||||
hass.data[PLATFORM][host] = xiaomi_air_purifier
|
||||
except DeviceException:
|
||||
raise PlatformNotReady
|
||||
hass.data[DATA_KEY][host] = device
|
||||
async_add_devices([device], update_before_add=True)
|
||||
|
||||
async_add_devices([xiaomi_air_purifier], update_before_add=True)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
async def async_service_handler(service):
|
||||
"""Map services to methods on XiaomiAirPurifier."""
|
||||
method = SERVICE_TO_METHOD.get(service.service)
|
||||
params = {key: value for key, value in service.data.items()
|
||||
if key != ATTR_ENTITY_ID}
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
if entity_ids:
|
||||
devices = [device for device in hass.data[PLATFORM].values() if
|
||||
devices = [device for device in hass.data[DATA_KEY].values() if
|
||||
device.entity_id in entity_ids]
|
||||
else:
|
||||
devices = hass.data[PLATFORM].values()
|
||||
devices = hass.data[DATA_KEY].values()
|
||||
|
||||
update_tasks = []
|
||||
for device in devices:
|
||||
yield from getattr(device, method['method'])(**params)
|
||||
if not hasattr(device, method['method']):
|
||||
continue
|
||||
await getattr(device, method['method'])(**params)
|
||||
update_tasks.append(device.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 air_purifier_service in SERVICE_TO_METHOD:
|
||||
schema = SERVICE_TO_METHOD[air_purifier_service].get(
|
||||
@@ -142,31 +390,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
DOMAIN, air_purifier_service, async_service_handler, schema=schema)
|
||||
|
||||
|
||||
class XiaomiAirPurifier(FanEntity):
|
||||
"""Representation of a Xiaomi Air Purifier."""
|
||||
class XiaomiGenericDevice(FanEntity):
|
||||
"""Representation of a generic Xiaomi device."""
|
||||
|
||||
def __init__(self, name, air_purifier):
|
||||
"""Initialize the air purifier."""
|
||||
def __init__(self, name, device, model, unique_id):
|
||||
"""Initialize the generic Xiaomi device."""
|
||||
self._name = name
|
||||
self._device = device
|
||||
self._model = model
|
||||
self._unique_id = unique_id
|
||||
|
||||
self._air_purifier = air_purifier
|
||||
self._available = False
|
||||
self._state = None
|
||||
self._state_attrs = {
|
||||
ATTR_AIR_QUALITY_INDEX: None,
|
||||
ATTR_TEMPERATURE: None,
|
||||
ATTR_HUMIDITY: None,
|
||||
ATTR_MODE: None,
|
||||
ATTR_FILTER_HOURS_USED: None,
|
||||
ATTR_FILTER_LIFE: None,
|
||||
ATTR_FAVORITE_LEVEL: None,
|
||||
ATTR_BUZZER: None,
|
||||
ATTR_CHILD_LOCK: None,
|
||||
ATTR_LED: None,
|
||||
ATTR_LED_BRIGHTNESS: None,
|
||||
ATTR_MOTOR_SPEED: None,
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: None,
|
||||
ATTR_PURIFY_VOLUME: None,
|
||||
ATTR_MODEL: self._model,
|
||||
}
|
||||
self._device_features = FEATURE_FLAGS_GENERIC
|
||||
self._skip_update = False
|
||||
|
||||
@property
|
||||
@@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll the fan."""
|
||||
"""Poll the device."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
@@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity):
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true when state is known."""
|
||||
return self._state is not None
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity):
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if fan is on."""
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def _try_command(self, mask_error, func, *args, **kwargs):
|
||||
"""Call an air purifier command handling error messages."""
|
||||
@staticmethod
|
||||
def _extract_value_from_attribute(state, attribute):
|
||||
value = getattr(state, attribute)
|
||||
if isinstance(value, Enum):
|
||||
return value.value
|
||||
|
||||
return value
|
||||
|
||||
async def _try_command(self, mask_error, func, *args, **kwargs):
|
||||
"""Call a miio device command handling error messages."""
|
||||
from miio import DeviceException
|
||||
try:
|
||||
result = yield from self.hass.async_add_job(
|
||||
result = await self.hass.async_add_job(
|
||||
partial(func, *args, **kwargs))
|
||||
|
||||
_LOGGER.debug("Response received from air purifier: %s", result)
|
||||
_LOGGER.debug("Response received from miio device: %s", result)
|
||||
|
||||
return result == SUCCESS
|
||||
except DeviceException as exc:
|
||||
_LOGGER.error(mask_error, exc)
|
||||
self._available = False
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
|
||||
"""Turn the fan on."""
|
||||
async def async_turn_on(self, speed: str = None,
|
||||
**kwargs) -> None:
|
||||
"""Turn the device on."""
|
||||
if speed:
|
||||
# If operation mode was set the device must not be turned on.
|
||||
result = yield from self.async_set_speed(speed)
|
||||
result = await self.async_set_speed(speed)
|
||||
else:
|
||||
result = yield from self._try_command(
|
||||
"Turning the air purifier on failed.", self._air_purifier.on)
|
||||
result = await self._try_command(
|
||||
"Turning the miio device on failed.", self._device.on)
|
||||
|
||||
if result:
|
||||
self._state = True
|
||||
self._skip_update = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self: ToggleEntity, **kwargs) -> None:
|
||||
"""Turn the fan off."""
|
||||
result = yield from self._try_command(
|
||||
"Turning the air purifier off failed.", self._air_purifier.off)
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the device off."""
|
||||
result = await self._try_command(
|
||||
"Turning the miio device off failed.", self._device.off)
|
||||
|
||||
if result:
|
||||
self._state = False
|
||||
self._skip_update = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_set_buzzer_on(self):
|
||||
"""Turn the buzzer on."""
|
||||
if self._device_features & FEATURE_SET_BUZZER == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the buzzer of the miio device on failed.",
|
||||
self._device.set_buzzer, True)
|
||||
|
||||
async def async_set_buzzer_off(self):
|
||||
"""Turn the buzzer off."""
|
||||
if self._device_features & FEATURE_SET_BUZZER == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the buzzer of the miio device off failed.",
|
||||
self._device.set_buzzer, False)
|
||||
|
||||
async def async_set_child_lock_on(self):
|
||||
"""Turn the child lock on."""
|
||||
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the child lock of the miio device on failed.",
|
||||
self._device.set_child_lock, True)
|
||||
|
||||
async def async_set_child_lock_off(self):
|
||||
"""Turn the child lock off."""
|
||||
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the child lock of the miio device off failed.",
|
||||
self._device.set_child_lock, False)
|
||||
|
||||
|
||||
class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||
"""Representation of a Xiaomi Air Purifier."""
|
||||
|
||||
def __init__(self, name, device, model, unique_id):
|
||||
"""Initialize the plug switch."""
|
||||
super().__init__(name, device, model, unique_id)
|
||||
|
||||
if self._model == MODEL_AIRPURIFIER_PRO:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO
|
||||
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO
|
||||
elif self._model == MODEL_AIRPURIFIER_V3:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3
|
||||
self._speed_list = OPERATION_MODES_AIRPURIFIER_V3
|
||||
else:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER
|
||||
self._speed_list = OPERATION_MODES_AIRPURIFIER
|
||||
|
||||
self._state_attrs.update(
|
||||
{attribute: None for attribute in self._available_attributes})
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch state from the device."""
|
||||
from miio import DeviceException
|
||||
|
||||
@@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity):
|
||||
return
|
||||
|
||||
try:
|
||||
state = yield from self.hass.async_add_job(
|
||||
self._air_purifier.status)
|
||||
state = await self.hass.async_add_job(
|
||||
self._device.status)
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
|
||||
self._available = True
|
||||
self._state = state.is_on
|
||||
self._state_attrs = {
|
||||
ATTR_TEMPERATURE: state.temperature,
|
||||
ATTR_HUMIDITY: state.humidity,
|
||||
ATTR_AIR_QUALITY_INDEX: state.aqi,
|
||||
ATTR_MODE: state.mode.value,
|
||||
ATTR_FILTER_HOURS_USED: state.filter_hours_used,
|
||||
ATTR_FILTER_LIFE: state.filter_life_remaining,
|
||||
ATTR_FAVORITE_LEVEL: state.favorite_level,
|
||||
ATTR_BUZZER: state.buzzer,
|
||||
ATTR_CHILD_LOCK: state.child_lock,
|
||||
ATTR_LED: state.led,
|
||||
ATTR_MOTOR_SPEED: state.motor_speed,
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi,
|
||||
ATTR_PURIFY_VOLUME: state.purify_volume,
|
||||
}
|
||||
|
||||
if state.led_brightness:
|
||||
self._state_attrs[
|
||||
ATTR_LED_BRIGHTNESS] = state.led_brightness.value
|
||||
self._state_attrs.update(
|
||||
{key: self._extract_value_from_attribute(state, value) for
|
||||
key, value in self._available_attributes.items()})
|
||||
|
||||
except DeviceException as ex:
|
||||
self._state = None
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
@property
|
||||
def speed_list(self: ToggleEntity) -> list:
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
from miio.airpurifier import OperationMode
|
||||
return [mode.name for mode in OperationMode]
|
||||
return self._speed_list
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
@@ -294,70 +588,228 @@ class XiaomiAirPurifier(FanEntity):
|
||||
|
||||
return None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
_LOGGER.debug("Setting the operation mode to: %s", speed)
|
||||
if self.supported_features & SUPPORT_SET_SPEED == 0:
|
||||
return
|
||||
|
||||
from miio.airpurifier import OperationMode
|
||||
|
||||
yield from self._try_command(
|
||||
"Setting operation mode of the air purifier failed.",
|
||||
self._air_purifier.set_mode, OperationMode[speed.title()])
|
||||
_LOGGER.debug("Setting the operation mode to: %s", speed)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_buzzer_on(self):
|
||||
"""Turn the buzzer on."""
|
||||
yield from self._try_command(
|
||||
"Turning the buzzer of the air purifier on failed.",
|
||||
self._air_purifier.set_buzzer, True)
|
||||
await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode, OperationMode[speed.title()])
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_buzzer_off(self):
|
||||
"""Turn the buzzer off."""
|
||||
yield from self._try_command(
|
||||
"Turning the buzzer of the air purifier off failed.",
|
||||
self._air_purifier.set_buzzer, False)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_led_on(self):
|
||||
async def async_set_led_on(self):
|
||||
"""Turn the led on."""
|
||||
yield from self._try_command(
|
||||
"Turning the led of the air purifier off failed.",
|
||||
self._air_purifier.set_led, True)
|
||||
if self._device_features & FEATURE_SET_LED == 0:
|
||||
return
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_led_off(self):
|
||||
await self._try_command(
|
||||
"Turning the led of the miio device off failed.",
|
||||
self._device.set_led, True)
|
||||
|
||||
async def async_set_led_off(self):
|
||||
"""Turn the led off."""
|
||||
yield from self._try_command(
|
||||
"Turning the led of the air purifier off failed.",
|
||||
self._air_purifier.set_led, False)
|
||||
if self._device_features & FEATURE_SET_LED == 0:
|
||||
return
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_child_lock_on(self):
|
||||
"""Turn the child lock on."""
|
||||
yield from self._try_command(
|
||||
"Turning the child lock of the air purifier on failed.",
|
||||
self._air_purifier.set_child_lock, True)
|
||||
await self._try_command(
|
||||
"Turning the led of the miio device off failed.",
|
||||
self._device.set_led, False)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_child_lock_off(self):
|
||||
"""Turn the child lock off."""
|
||||
yield from self._try_command(
|
||||
"Turning the child lock of the air purifier off failed.",
|
||||
self._air_purifier.set_child_lock, False)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_led_brightness(self, brightness: int = 2):
|
||||
async def async_set_led_brightness(self, brightness: int = 2):
|
||||
"""Set the led brightness."""
|
||||
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
|
||||
return
|
||||
|
||||
from miio.airpurifier import LedBrightness
|
||||
|
||||
yield from self._try_command(
|
||||
"Setting the led brightness of the air purifier failed.",
|
||||
self._air_purifier.set_led_brightness, LedBrightness(brightness))
|
||||
await self._try_command(
|
||||
"Setting the led brightness of the miio device failed.",
|
||||
self._device.set_led_brightness, LedBrightness(brightness))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_favorite_level(self, level: int = 1):
|
||||
async def async_set_favorite_level(self, level: int = 1):
|
||||
"""Set the favorite level."""
|
||||
yield from self._try_command(
|
||||
"Setting the favorite level of the air purifier failed.",
|
||||
self._air_purifier.set_favorite_level, level)
|
||||
if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Setting the favorite level of the miio device failed.",
|
||||
self._device.set_favorite_level, level)
|
||||
|
||||
async def async_set_auto_detect_on(self):
|
||||
"""Turn the auto detect on."""
|
||||
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the auto detect of the miio device on failed.",
|
||||
self._device.set_auto_detect, True)
|
||||
|
||||
async def async_set_auto_detect_off(self):
|
||||
"""Turn the auto detect off."""
|
||||
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the auto detect of the miio device off failed.",
|
||||
self._device.set_auto_detect, False)
|
||||
|
||||
async def async_set_learn_mode_on(self):
|
||||
"""Turn the learn mode on."""
|
||||
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the learn mode of the miio device on failed.",
|
||||
self._device.set_learn_mode, True)
|
||||
|
||||
async def async_set_learn_mode_off(self):
|
||||
"""Turn the learn mode off."""
|
||||
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the learn mode of the miio device off failed.",
|
||||
self._device.set_learn_mode, False)
|
||||
|
||||
async def async_set_volume(self, volume: int = 50):
|
||||
"""Set the sound volume."""
|
||||
if self._device_features & FEATURE_SET_VOLUME == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Setting the sound volume of the miio device failed.",
|
||||
self._device.set_volume, volume)
|
||||
|
||||
async def async_set_extra_features(self, features: int = 1):
|
||||
"""Set the extra features."""
|
||||
if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Setting the extra features of the miio device failed.",
|
||||
self._device.set_extra_features, features)
|
||||
|
||||
async def async_reset_filter(self):
|
||||
"""Reset the filter lifetime and usage."""
|
||||
if self._device_features & FEATURE_RESET_FILTER == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Resetting the filter lifetime of the miio device failed.",
|
||||
self._device.reset_filter)
|
||||
|
||||
|
||||
class XiaomiAirHumidifier(XiaomiGenericDevice):
|
||||
"""Representation of a Xiaomi Air Humidifier."""
|
||||
|
||||
def __init__(self, name, device, model, unique_id):
|
||||
"""Initialize the plug switch."""
|
||||
from miio.airhumidifier import OperationMode
|
||||
|
||||
super().__init__(name, device, model, unique_id)
|
||||
|
||||
if self._model == MODEL_AIRHUMIDIFIER_CA:
|
||||
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA
|
||||
self._speed_list = [mode.name for mode in OperationMode]
|
||||
else:
|
||||
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
|
||||
self._speed_list = [mode.name for mode in OperationMode if
|
||||
mode.name != 'Auto']
|
||||
|
||||
self._state_attrs.update(
|
||||
{attribute: None for attribute in self._available_attributes})
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch state from the device."""
|
||||
from miio import DeviceException
|
||||
|
||||
# On state change the device doesn't provide the new state immediately.
|
||||
if self._skip_update:
|
||||
self._skip_update = False
|
||||
return
|
||||
|
||||
try:
|
||||
state = await self.hass.async_add_job(self._device.status)
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
|
||||
self._available = True
|
||||
self._state = state.is_on
|
||||
self._state_attrs.update(
|
||||
{key: self._extract_value_from_attribute(state, value) for
|
||||
key, value in self._available_attributes.items()})
|
||||
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return self._speed_list
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
"""Return the current speed."""
|
||||
if self._state:
|
||||
from miio.airhumidifier import OperationMode
|
||||
|
||||
return OperationMode(self._state_attrs[ATTR_MODE]).name
|
||||
|
||||
return None
|
||||
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
if self.supported_features & SUPPORT_SET_SPEED == 0:
|
||||
return
|
||||
|
||||
from miio.airhumidifier import OperationMode
|
||||
|
||||
_LOGGER.debug("Setting the operation mode to: %s", speed)
|
||||
|
||||
await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode, OperationMode[speed.title()])
|
||||
|
||||
async def async_set_led_brightness(self, brightness: int = 2):
|
||||
"""Set the led brightness."""
|
||||
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
|
||||
return
|
||||
|
||||
from miio.airhumidifier import LedBrightness
|
||||
|
||||
await self._try_command(
|
||||
"Setting the led brightness of the miio device failed.",
|
||||
self._device.set_led_brightness, LedBrightness(brightness))
|
||||
|
||||
async def async_set_target_humidity(self, humidity: int = 40):
|
||||
"""Set the target humidity."""
|
||||
if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Setting the target humidity of the miio device failed.",
|
||||
self._device.set_target_humidity, humidity)
|
||||
|
||||
async def async_set_dry_on(self):
|
||||
"""Turn the dry mode on."""
|
||||
if self._device_features & FEATURE_SET_DRY == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the dry mode of the miio device off failed.",
|
||||
self._device.set_dry, True)
|
||||
|
||||
async def async_set_dry_off(self):
|
||||
"""Turn the dry mode off."""
|
||||
if self._device_features & FEATURE_SET_DRY == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the dry mode of the miio device off failed.",
|
||||
self._device.set_dry, False)
|
||||
|
||||
114
homeassistant/components/fan/zha.py
Normal file
114
homeassistant/components/fan/zha.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Fans on Zigbee Home Automation networks.
|
||||
|
||||
For more details on this platform, please refer to the documentation
|
||||
at https://home-assistant.io/components/fan.zha/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from homeassistant.components import zha
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
DEPENDENCIES = ['zha']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Additional speeds in zigbee's ZCL
|
||||
# Spec is unclear as to what this value means. On King Of Fans HBUniversal
|
||||
# receiver, this means Very High.
|
||||
SPEED_ON = 'on'
|
||||
# The fan speed is self-regulated
|
||||
SPEED_AUTO = 'auto'
|
||||
# When the heated/cooled space is occupied, the fan is always on
|
||||
SPEED_SMART = 'smart'
|
||||
|
||||
SPEED_LIST = [
|
||||
SPEED_OFF,
|
||||
SPEED_LOW,
|
||||
SPEED_MEDIUM,
|
||||
SPEED_HIGH,
|
||||
SPEED_ON,
|
||||
SPEED_AUTO,
|
||||
SPEED_SMART
|
||||
]
|
||||
|
||||
VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)}
|
||||
SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Zigbee Home Automation fans."""
|
||||
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
async_add_devices([ZhaFan(**discovery_info)], update_before_add=True)
|
||||
|
||||
|
||||
class ZhaFan(zha.Entity, FanEntity):
|
||||
"""Representation of a ZHA fan."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_SET_SPEED
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return SPEED_LIST
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if entity is on."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return False
|
||||
return self._state != SPEED_OFF
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, speed: str = None, **kwargs) -> None:
|
||||
"""Turn the entity on."""
|
||||
if speed is None:
|
||||
speed = SPEED_MEDIUM
|
||||
|
||||
yield from self.async_set_speed(speed)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the entity off."""
|
||||
yield from self.async_set_speed(SPEED_OFF)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_speed(self: FanEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
yield from self._endpoint.fan.write_attributes({
|
||||
'fan_mode': SPEED_TO_VALUE[speed]})
|
||||
|
||||
self._state = speed
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode'])
|
||||
new_value = result.get('fan_mode', None)
|
||||
self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if entity has to be polled for state.
|
||||
|
||||
False if entity pushes its state to HA.
|
||||
"""
|
||||
return False
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user