forked from home-assistant/core
Compare commits
476 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b9e59cee | ||
|
|
fb447cab82 | ||
|
|
6b9addfeea | ||
|
|
6c62f7231b | ||
|
|
52c21a53b3 | ||
|
|
c9498d9f09 | ||
|
|
2f0435ebd8 | ||
|
|
19351fc429 | ||
|
|
bfc16428da | ||
|
|
43d2e436b9 | ||
|
|
ef35b8d428 | ||
|
|
69e86c29a6 | ||
|
|
a4d45c46e8 | ||
|
|
45d1d30a8b | ||
|
|
4671bd95c6 | ||
|
|
1bc916927c | ||
|
|
2f8865d6cb | ||
|
|
cfdea8d20f | ||
|
|
ba9bb90cf7 | ||
|
|
8854efd685 | ||
|
|
b0e850ba5d | ||
|
|
f2dfc84d52 | ||
|
|
e55f7ebf81 | ||
|
|
8d06469efe | ||
|
|
c1127133ea | ||
|
|
25970027c6 | ||
|
|
d7640e6ec3 | ||
|
|
12e76ef7c1 | ||
|
|
d36996c8f0 | ||
|
|
e929f45ab8 | ||
|
|
4c328baaa6 | ||
|
|
cc5edf69e3 | ||
|
|
909f2448ca | ||
|
|
97076aa3fd | ||
|
|
a3777c4ea8 | ||
|
|
1c3293ac85 | ||
|
|
f06a0ba373 | ||
|
|
9afc2634c6 | ||
|
|
ed3efc8712 | ||
|
|
144524fbbb | ||
|
|
298d31e42b | ||
|
|
3e7d4fc902 | ||
|
|
64223cea72 | ||
|
|
1053473111 | ||
|
|
25dcddfeef | ||
|
|
e20f88c143 | ||
|
|
1533a68c06 | ||
|
|
5ff5c73e2b | ||
|
|
6ba49e12a2 | ||
|
|
852ce9f990 | ||
|
|
df69680d24 | ||
|
|
2e7b5dcd19 | ||
|
|
e49e0b5a13 | ||
|
|
de50d5d9c1 | ||
|
|
612a37b2dd | ||
|
|
d47006c98f | ||
|
|
16bf10b1a2 | ||
|
|
710533ae8a | ||
|
|
11c57f9345 | ||
|
|
7562b4164b | ||
|
|
cf44b77225 | ||
|
|
44e9783c7c | ||
|
|
1b5c02ff67 | ||
|
|
2f74ffcf81 | ||
|
|
954e4796b8 | ||
|
|
fb501282cc | ||
|
|
c06351f2a9 | ||
|
|
6b9c65c9ce | ||
|
|
8ae3caa292 | ||
|
|
391e3196ea | ||
|
|
cb709931e4 | ||
|
|
a750f8444e | ||
|
|
a5bff4cd8d | ||
|
|
e0bc894cbb | ||
|
|
3ec56d55c5 | ||
|
|
b904a4e770 | ||
|
|
146a9492ec | ||
|
|
e5d714ef52 | ||
|
|
4d63baf705 | ||
|
|
234bf1f0ea | ||
|
|
843789528e | ||
|
|
ea2c073612 | ||
|
|
d1228d5cf4 | ||
|
|
7aec098a05 | ||
|
|
70af7e5fad | ||
|
|
99e272fc8d | ||
|
|
990f476ac9 | ||
|
|
9abc13aaa6 | ||
|
|
d17186a8b7 | ||
|
|
6fedad7890 | ||
|
|
b371bf700f | ||
|
|
01ce43ec7c | ||
|
|
b903bbc042 | ||
|
|
304137e7ff | ||
|
|
e80628d45b | ||
|
|
d6b81fb345 | ||
|
|
c5cac04e54 | ||
|
|
621c653fed | ||
|
|
48d70e520f | ||
|
|
528ad56530 | ||
|
|
be3b227a87 | ||
|
|
ef8fc1f201 | ||
|
|
8c0b45af1e | ||
|
|
3b39ab5b94 | ||
|
|
8fcf085829 | ||
|
|
6843893d9f | ||
|
|
e963fc5acf | ||
|
|
bc664c276c | ||
|
|
f192ef8219 | ||
|
|
db31cdf075 | ||
|
|
f168226be9 | ||
|
|
ea01b127c2 | ||
|
|
6e831138b4 | ||
|
|
eb2671f4bb | ||
|
|
8d017b7678 | ||
|
|
5ec7fc7ddb | ||
|
|
0f3ec94fba | ||
|
|
2c566072f5 | ||
|
|
cf8562a030 | ||
|
|
a91c1bc668 | ||
|
|
f406fd57ac | ||
|
|
2d0e3c1402 | ||
|
|
01ec4a7afd | ||
|
|
d43e6a2888 | ||
|
|
50cea77887 | ||
|
|
6231394614 | ||
|
|
f516cc7dc6 | ||
|
|
9c7523d7b0 | ||
|
|
10505d542a | ||
|
|
e12994a0cd | ||
|
|
ff01aa40c9 | ||
|
|
6199e50e80 | ||
|
|
c664c20165 | ||
|
|
eb551a6d5a | ||
|
|
4343659742 | ||
|
|
230bd3929c | ||
|
|
ba7333e804 | ||
|
|
48b13cc865 | ||
|
|
e7c7b9b2a9 | ||
|
|
8d24541ffe | ||
|
|
b1eb35ee11 | ||
|
|
c7166241f7 | ||
|
|
a4e1615127 | ||
|
|
3e5d76efb2 | ||
|
|
6a74fa344d | ||
|
|
c4ec2e3434 | ||
|
|
c48986a467 | ||
|
|
ab621808bd | ||
|
|
5c88e897af | ||
|
|
6318178a8b | ||
|
|
a2b8ad50f2 | ||
|
|
5c95c53c6c | ||
|
|
e60d066514 | ||
|
|
91fe6e4e56 | ||
|
|
34727be5ac | ||
|
|
107769ab81 | ||
|
|
63cc179ea2 | ||
|
|
2bb1a95098 | ||
|
|
f3411f8db2 | ||
|
|
1e31af77de | ||
|
|
2326312bee | ||
|
|
83e342daf2 | ||
|
|
a4b69833d4 | ||
|
|
64ba2c63c7 | ||
|
|
2e8eaf40f7 | ||
|
|
b9e893184a | ||
|
|
4d085882d5 | ||
|
|
f6e29a6647 | ||
|
|
1a936220e9 | ||
|
|
8410b63d9c | ||
|
|
af8cd63838 | ||
|
|
95d27bd1fa | ||
|
|
ec3ce4c80d | ||
|
|
5ade84d75f | ||
|
|
75bf483071 | ||
|
|
354470469f | ||
|
|
255a85ad02 | ||
|
|
bb76ba67f3 | ||
|
|
7900ba30bf | ||
|
|
e37fd5b132 | ||
|
|
f98525acbf | ||
|
|
36cf2125ce | ||
|
|
c80b752d0e | ||
|
|
fa0ad7b317 | ||
|
|
b49d98407c | ||
|
|
5f8f6666e6 | ||
|
|
54ccbbcb64 | ||
|
|
a7a3cff0f1 | ||
|
|
9859840b9c | ||
|
|
8cabec7ac1 | ||
|
|
15e75b07d8 | ||
|
|
58257af289 | ||
|
|
4ecce2598a | ||
|
|
e68b52d50d | ||
|
|
c9de2f015b | ||
|
|
ef4498ec27 | ||
|
|
c851dfa2c7 | ||
|
|
64b9fbd8d9 | ||
|
|
f72d568374 | ||
|
|
351e8921fa | ||
|
|
b66be59598 | ||
|
|
14c7fa8882 | ||
|
|
ce98dfe395 | ||
|
|
bf056b6f01 | ||
|
|
8b13658d3b | ||
|
|
e968b1a0f4 | ||
|
|
6453ea4e61 | ||
|
|
c2d00be91e | ||
|
|
e4655a7e63 | ||
|
|
7a05471912 | ||
|
|
8d5c3a2b91 | ||
|
|
2f0fc0934f | ||
|
|
83d300fd11 | ||
|
|
5d96751168 | ||
|
|
38560cda1c | ||
|
|
bf53cbe08d | ||
|
|
b00f771541 | ||
|
|
9bc8f6649b | ||
|
|
b0cccbfd9f | ||
|
|
e78497789b | ||
|
|
d82693b460 | ||
|
|
cdd45e7878 | ||
|
|
b994c10d7f | ||
|
|
9d4d1c8233 | ||
|
|
a4e0c9c251 | ||
|
|
626d6df545 | ||
|
|
12dff5baa8 | ||
|
|
d1460de89b | ||
|
|
c23cc0e827 | ||
|
|
c704ceaeb7 | ||
|
|
daeccfe764 | ||
|
|
7f1b591fbb | ||
|
|
aba143ac9f | ||
|
|
03c34804bc | ||
|
|
f2a17a5462 | ||
|
|
b5bae17c66 | ||
|
|
52a48b3ac9 | ||
|
|
a06f61034c | ||
|
|
6e0a3abf66 | ||
|
|
eceece866d | ||
|
|
853a16938b | ||
|
|
5dcad89a0d | ||
|
|
46c260fd85 | ||
|
|
76c9c0179b | ||
|
|
d7eced95fa | ||
|
|
02a12a0bb4 | ||
|
|
30d987f59f | ||
|
|
aa8bd37143 | ||
|
|
4c0024fd97 | ||
|
|
74320306a1 | ||
|
|
113bdc493a | ||
|
|
8e7f500f28 | ||
|
|
d352dee9b7 | ||
|
|
3fd4987baf | ||
|
|
ef48a7ca2c | ||
|
|
fd038b6de9 | ||
|
|
a4bf421044 | ||
|
|
a0b14c2913 | ||
|
|
44ddc6ba62 | ||
|
|
07f94eaa92 | ||
|
|
4205dc0f7c | ||
|
|
2091f86e25 | ||
|
|
84f163252a | ||
|
|
9a9161477f | ||
|
|
449085313b | ||
|
|
95f2ad2299 | ||
|
|
7bdd4dd960 | ||
|
|
e6d4501ee3 | ||
|
|
93fe61bf13 | ||
|
|
b352b761f3 | ||
|
|
8d87b9fed5 | ||
|
|
ea5c336ab4 | ||
|
|
c78e8eb578 | ||
|
|
8bc497ba1d | ||
|
|
1d41321f8f | ||
|
|
00706ad90c | ||
|
|
2749ca4ef4 | ||
|
|
58ae8d91f9 | ||
|
|
9d34e8c266 | ||
|
|
7da1d75707 | ||
|
|
7e39a5c4d5 | ||
|
|
9fb2bf72f9 | ||
|
|
c42c668815 | ||
|
|
9d0251cfeb | ||
|
|
403a546bdc | ||
|
|
0b350993b5 | ||
|
|
9d1f9fe204 | ||
|
|
4b06392442 | ||
|
|
833508fbbb | ||
|
|
1b71ce32e4 | ||
|
|
f5de2b9e5b | ||
|
|
3e18078700 | ||
|
|
ff7b51259e | ||
|
|
47e143d5a1 | ||
|
|
d7f7735490 | ||
|
|
8c2dedab52 | ||
|
|
241a0793bb | ||
|
|
a94864c86f | ||
|
|
f23f9465d3 | ||
|
|
558b659f7c | ||
|
|
0a0d34d394 | ||
|
|
c49751542f | ||
|
|
2e3a27e418 | ||
|
|
7566bb5aed | ||
|
|
fc1f6ee0f0 | ||
|
|
cb839eff0f | ||
|
|
2bc87bfcf0 | ||
|
|
44be80145b | ||
|
|
8cb1e17ad8 | ||
|
|
75fffb6a86 | ||
|
|
4e97954bbe | ||
|
|
18137733f9 | ||
|
|
ca29224846 | ||
|
|
31554e8368 | ||
|
|
5ed73fecd3 | ||
|
|
8a10fcd985 | ||
|
|
5fe4053021 | ||
|
|
e4cb3af76d | ||
|
|
7f634c6ed0 | ||
|
|
5d3471269a | ||
|
|
1fbc650871 | ||
|
|
86374ad809 | ||
|
|
c2bee496e2 | ||
|
|
51f55bddb7 | ||
|
|
4c23a61853 | ||
|
|
f12ff6f297 | ||
|
|
cb490780c9 | ||
|
|
6ccb83584e | ||
|
|
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 | ||
|
|
2cb9e2dc7c | ||
|
|
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 | ||
|
|
ac2298189e | ||
|
|
60508f7215 | ||
|
|
ddd2003629 | ||
|
|
20ababec3e | ||
|
|
d3b261a25d | ||
|
|
3906250c9e | ||
|
|
22a1b99e57 | ||
|
|
62dc737ea3 | ||
|
|
993866a314 | ||
|
|
51bdd06d1f | ||
|
|
d2804b0a27 | ||
|
|
c863b9614c | ||
|
|
f47572d3c0 | ||
|
|
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 | ||
|
|
85487612d5 |
45
.coveragerc
45
.coveragerc
@@ -4,6 +4,8 @@ source = homeassistant
|
||||
omit =
|
||||
homeassistant/__main__.py
|
||||
homeassistant/scripts/*.py
|
||||
homeassistant/util/async.py
|
||||
homeassistant/monkey_patch.py
|
||||
homeassistant/helpers/typing.py
|
||||
homeassistant/helpers/signal.py
|
||||
|
||||
@@ -29,7 +31,7 @@ omit =
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
homeassistant/components/bmw_connected_drive.py
|
||||
homeassistant/components/bmw_connected_drive/*.py
|
||||
homeassistant/components/*/bmw_connected_drive.py
|
||||
|
||||
homeassistant/components/android_ip_webcam.py
|
||||
@@ -94,6 +96,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,6 +114,9 @@ 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
|
||||
|
||||
@@ -118,7 +129,7 @@ omit =
|
||||
homeassistant/components/insteon_local.py
|
||||
homeassistant/components/*/insteon_local.py
|
||||
|
||||
homeassistant/components/insteon_plm.py
|
||||
homeassistant/components/insteon_plm/*
|
||||
homeassistant/components/*/insteon_plm.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
@@ -142,6 +153,9 @@ omit =
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/konnected.py
|
||||
homeassistant/components/*/konnected.py
|
||||
|
||||
homeassistant/components/lametric.py
|
||||
homeassistant/components/*/lametric.py
|
||||
|
||||
@@ -157,6 +171,9 @@ omit =
|
||||
homeassistant/components/mailgun.py
|
||||
homeassistant/components/*/mailgun.py
|
||||
|
||||
homeassistant/components/matrix.py
|
||||
homeassistant/components/*/matrix.py
|
||||
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
@@ -190,8 +207,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
|
||||
@@ -199,6 +216,9 @@ omit =
|
||||
homeassistant/components/raincloud.py
|
||||
homeassistant/components/*/raincloud.py
|
||||
|
||||
homeassistant/components/rainmachine.py
|
||||
homeassistant/components/*/rainmachine.py
|
||||
|
||||
homeassistant/components/raspihats.py
|
||||
homeassistant/components/*/raspihats.py
|
||||
|
||||
@@ -211,6 +231,9 @@ omit =
|
||||
homeassistant/components/rpi_pfio.py
|
||||
homeassistant/components/*/rpi_pfio.py
|
||||
|
||||
homeassistant/components/sabnzbd.py
|
||||
homeassistant/components/*/sabnzbd.py
|
||||
|
||||
homeassistant/components/satel_integra.py
|
||||
homeassistant/components/*/satel_integra.py
|
||||
|
||||
@@ -327,6 +350,7 @@ omit =
|
||||
homeassistant/components/calendar/todoist.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/canary.py
|
||||
homeassistant/components/camera/familyhub.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
@@ -397,7 +421,6 @@ omit =
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/folder_watcher.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/goalfeed.py
|
||||
@@ -507,7 +530,6 @@ omit =
|
||||
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
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
@@ -520,7 +542,6 @@ omit =
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/rocketchat.py
|
||||
homeassistant/components/notify/sendgrid.py
|
||||
homeassistant/components/notify/simplepush.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/stride.py
|
||||
@@ -565,6 +586,7 @@ omit =
|
||||
homeassistant/components/sensor/discogs.py
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/domain_expiry.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/dwd_weather_warnings.py
|
||||
@@ -577,6 +599,7 @@ omit =
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/filesize.py
|
||||
homeassistant/components/sensor/fints.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
@@ -606,6 +629,7 @@ omit =
|
||||
homeassistant/components/sensor/lyft.py
|
||||
homeassistant/components/sensor/metoffice.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/mitemp_bt.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mopar.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
@@ -626,6 +650,7 @@ omit =
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
homeassistant/components/sensor/pollen.py
|
||||
homeassistant/components/sensor/postnl.py
|
||||
homeassistant/components/sensor/pushbullet.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/pyload.py
|
||||
@@ -633,18 +658,20 @@ omit =
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/sht31.py
|
||||
homeassistant/components/sensor/shodan.py
|
||||
homeassistant/components/sensor/sigfox.py
|
||||
homeassistant/components/sensor/simulated.py
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
homeassistant/components/sensor/sma.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sochain.py
|
||||
homeassistant/components/sensor/socialblade.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/spotcrime.py
|
||||
@@ -669,6 +696,7 @@ omit =
|
||||
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
|
||||
@@ -698,7 +726,6 @@ omit =
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rainbird.py
|
||||
homeassistant/components/switch/rainmachine.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/snmp.py
|
||||
|
||||
50
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
50
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
<!-- 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!
|
||||
-->
|
||||
|
||||
**Home Assistant release with the issue:**
|
||||
<!--
|
||||
- Frontend -> Developer tools -> Info
|
||||
- Or use this command: hass --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:**
|
||||
|
||||
|
||||
|
||||
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
**Traceback (if applicable):**
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
**Additional information:**
|
||||
@@ -10,8 +10,8 @@ matrix:
|
||||
env: TOXENV=lint
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=pylint
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
@@ -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
|
||||
|
||||
10
CODEOWNERS
10
CODEOWNERS
@@ -54,8 +54,11 @@ homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/lock/nello.py @pschmitt
|
||||
homeassistant/components/lock/nuki.py @pschmitt
|
||||
homeassistant/components/media_player/emby.py @mezz64
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/liveboxplaytv.py @pschmitt
|
||||
homeassistant/components/media_player/mediaroom.py @dgomes
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
homeassistant/components/media_player/sonos.py @amelchio
|
||||
@@ -63,6 +66,7 @@ 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
|
||||
@@ -72,9 +76,11 @@ 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
|
||||
homeassistant/components/vacuum/roomba.py @pschmitt
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
homeassistant/components/*/axis.py @kane610
|
||||
@@ -88,6 +94,10 @@ homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
homeassistant/components/konnected.py @heythisisnate
|
||||
homeassistant/components/*/konnected.py @heythisisnate
|
||||
homeassistant/components/matrix.py @tinloaf
|
||||
homeassistant/components/*/matrix.py @tinloaf
|
||||
homeassistant/components/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
|
||||
@@ -8,7 +8,8 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import
|
||||
|
||||
|
||||
from homeassistant import monkey_patch
|
||||
from homeassistant.const import (
|
||||
@@ -126,6 +127,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',
|
||||
@@ -255,17 +260,18 @@ def setup_and_run_hass(config_dir: str,
|
||||
config = {
|
||||
'frontend': {},
|
||||
'demo': {}
|
||||
}
|
||||
} # type: Dict[str, Any]
|
||||
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
|
||||
|
||||
503
homeassistant/auth.py
Normal file
503
homeassistant/auth.py
Normal file
@@ -0,0 +1,503 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
import asyncio
|
||||
import binascii
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import importlib
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
AUTH_PROVIDERS = Registry()
|
||||
|
||||
AUTH_PROVIDER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): str,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
# Specify ID if you have two auth providers for same type.
|
||||
vol.Optional(CONF_ID): str,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
DATA_REQS = 'auth_reqs_processed'
|
||||
|
||||
|
||||
def generate_secret(entropy: int = 32) -> str:
|
||||
"""Generate a secret.
|
||||
|
||||
Backport of secrets.token_hex from Python 3.6
|
||||
|
||||
Event loop friendly.
|
||||
"""
|
||||
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
|
||||
|
||||
|
||||
class AuthProvider:
|
||||
"""Provider of user authentication."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth provider'
|
||||
|
||||
initialized = False
|
||||
|
||||
def __init__(self, hass, store, config):
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""Return id of the auth provider.
|
||||
|
||||
Optional, can be None.
|
||||
"""
|
||||
return self.config.get(CONF_ID)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Return type of the provider."""
|
||||
return self.config[CONF_TYPE]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the auth provider."""
|
||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||
|
||||
async def async_credentials(self):
|
||||
"""Return all credentials of this provider."""
|
||||
return await self.store.credentials_for_provider(self.type, self.id)
|
||||
|
||||
@callback
|
||||
def async_create_credentials(self, data):
|
||||
"""Create credentials."""
|
||||
return Credentials(
|
||||
auth_provider_type=self.type,
|
||||
auth_provider_id=self.id,
|
||||
data=data,
|
||||
)
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_initialize(self):
|
||||
"""Initialize the auth provider.
|
||||
|
||||
Optional.
|
||||
"""
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return the data flow for logging in with auth provider."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class User:
|
||||
"""A user."""
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_owner = attr.ib(type=bool, default=False)
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
name = attr.ib(type=str, default=None)
|
||||
# For persisting and see if saved?
|
||||
# store = attr.ib(type=AuthStore, default=None)
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
# Tokens associated with a user.
|
||||
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict))
|
||||
|
||||
def as_dict(self):
|
||||
"""Convert user object to a dictionary."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'is_owner': self.is_owner,
|
||||
'is_active': self.is_active,
|
||||
'name': self.name,
|
||||
}
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class RefreshToken:
|
||||
"""RefreshToken for a user to grant new access tokens."""
|
||||
|
||||
user = attr.ib(type=User)
|
||||
client_id = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
access_token_expiration = attr.ib(type=timedelta,
|
||||
default=ACCESS_TOKEN_EXPIRATION)
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
access_tokens = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class AccessToken:
|
||||
"""Access token to access the API.
|
||||
|
||||
These will only ever be stored in memory and not be persisted.
|
||||
"""
|
||||
|
||||
refresh_token = attr.ib(type=RefreshToken)
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(generate_secret))
|
||||
|
||||
@property
|
||||
def expires(self):
|
||||
"""Return datetime when this token expires."""
|
||||
return self.created_at + self.refresh_token.access_token_expiration
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Credentials:
|
||||
"""Credentials for a user on an auth provider."""
|
||||
|
||||
auth_provider_type = attr.ib(type=str)
|
||||
auth_provider_id = attr.ib(type=str)
|
||||
|
||||
# Allow the auth provider to store data to represent their auth.
|
||||
data = attr.ib(type=dict)
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_new = attr.ib(type=bool, default=True)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class Client:
|
||||
"""Client that interacts with Home Assistant on behalf of a user."""
|
||||
|
||||
name = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
secret = attr.ib(type=str, default=attr.Factory(generate_secret))
|
||||
redirect_uris = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
|
||||
async def load_auth_provider_module(hass, provider):
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
'homeassistant.auth_providers.{}'.format(provider))
|
||||
except ImportError:
|
||||
_LOGGER.warning('Unable to find auth provider %s', provider)
|
||||
return None
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
|
||||
if processed is None:
|
||||
processed = hass.data[DATA_REQS] = set()
|
||||
elif provider in processed:
|
||||
return module
|
||||
|
||||
req_success = await requirements.async_process_requirements(
|
||||
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
|
||||
return module
|
||||
|
||||
|
||||
async def auth_manager_from_config(hass, provider_configs):
|
||||
"""Initialize an auth manager from config."""
|
||||
store = AuthStore(hass)
|
||||
if provider_configs:
|
||||
providers = await asyncio.gather(
|
||||
*[_auth_provider_from_config(hass, store, config)
|
||||
for config in provider_configs])
|
||||
else:
|
||||
providers = []
|
||||
# So returned auth providers are in same order as config
|
||||
provider_hash = OrderedDict()
|
||||
for provider in providers:
|
||||
if provider is None:
|
||||
continue
|
||||
|
||||
key = (provider.type, provider.id)
|
||||
|
||||
if key in provider_hash:
|
||||
_LOGGER.error(
|
||||
'Found duplicate provider: %s. Please add unique IDs if you '
|
||||
'want to have the same provider twice.', key)
|
||||
continue
|
||||
|
||||
provider_hash[key] = provider
|
||||
manager = AuthManager(hass, store, provider_hash)
|
||||
return manager
|
||||
|
||||
|
||||
async def _auth_provider_from_config(hass, store, config):
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
module = await load_auth_provider_module(hass, provider_name)
|
||||
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||
provider_name, humanize_error(config, err))
|
||||
return None
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config)
|
||||
|
||||
|
||||
class AuthManager:
|
||||
"""Manage the authentication for Home Assistant."""
|
||||
|
||||
def __init__(self, hass, store, providers):
|
||||
"""Initialize the auth manager."""
|
||||
self._store = store
|
||||
self._providers = providers
|
||||
self.login_flow = data_entry_flow.FlowManager(
|
||||
hass, self._async_create_login_flow,
|
||||
self._async_finish_login_flow)
|
||||
self.access_tokens = {}
|
||||
|
||||
@property
|
||||
def async_auth_providers(self):
|
||||
"""Return a list of available auth providers."""
|
||||
return self._providers.values()
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user."""
|
||||
return await self._store.async_get_user(user_id)
|
||||
|
||||
async def async_get_or_create_user(self, credentials):
|
||||
"""Get or create a user."""
|
||||
return await self._store.async_get_or_create_user(
|
||||
credentials, self._async_get_auth_provider(credentials))
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Link credentials to an existing user."""
|
||||
await self._store.async_link_user(user, credentials)
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
await self._store.async_remove_user(user)
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id):
|
||||
"""Create a new refresh token for a user."""
|
||||
return await self._store.async_create_refresh_token(user, client_id)
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
return await self._store.async_get_refresh_token(token)
|
||||
|
||||
@callback
|
||||
def async_create_access_token(self, refresh_token):
|
||||
"""Create a new access token."""
|
||||
access_token = AccessToken(refresh_token)
|
||||
self.access_tokens[access_token.token] = access_token
|
||||
return access_token
|
||||
|
||||
@callback
|
||||
def async_get_access_token(self, token):
|
||||
"""Get an access token."""
|
||||
return self.access_tokens.get(token)
|
||||
|
||||
async def async_create_client(self, name, *, redirect_uris=None,
|
||||
no_secret=False):
|
||||
"""Create a new client."""
|
||||
return await self._store.async_create_client(
|
||||
name, redirect_uris, no_secret)
|
||||
|
||||
async def async_get_client(self, client_id):
|
||||
"""Get a client."""
|
||||
return await self._store.async_get_client(client_id)
|
||||
|
||||
async def _async_create_login_flow(self, handler, *, source, data):
|
||||
"""Create a login flow."""
|
||||
auth_provider = self._providers[handler]
|
||||
|
||||
if not auth_provider.initialized:
|
||||
auth_provider.initialized = True
|
||||
await auth_provider.async_initialize()
|
||||
|
||||
return await auth_provider.async_credential_flow()
|
||||
|
||||
async def _async_finish_login_flow(self, result):
|
||||
"""Result of a credential login flow."""
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return None
|
||||
|
||||
auth_provider = self._providers[result['handler']]
|
||||
return await auth_provider.async_get_or_create_credentials(
|
||||
result['data'])
|
||||
|
||||
@callback
|
||||
def _async_get_auth_provider(self, credentials):
|
||||
"""Helper to get auth provider from a set of credentials."""
|
||||
auth_provider_key = (credentials.auth_provider_type,
|
||||
credentials.auth_provider_id)
|
||||
return self._providers[auth_provider_key]
|
||||
|
||||
|
||||
class AuthStore:
|
||||
"""Stores authentication info.
|
||||
|
||||
Any mutation to an object should happen inside the auth store.
|
||||
|
||||
The auth store is lazy. It won't load the data from disk until a method is
|
||||
called that needs it.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self.users = None
|
||||
self.clients = None
|
||||
self._load_lock = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
async def credentials_for_provider(self, provider_type, provider_id):
|
||||
"""Return credentials for specific auth provider type and id."""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
return [
|
||||
credentials
|
||||
for user in self.users.values()
|
||||
for credentials in user.credentials
|
||||
if (credentials.auth_provider_type == provider_type and
|
||||
credentials.auth_provider_id == provider_id)
|
||||
]
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
"""Retrieve a user."""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
return self.users.get(user_id)
|
||||
|
||||
async def async_get_or_create_user(self, credentials, auth_provider):
|
||||
"""Get or create a new user for given credentials.
|
||||
|
||||
If link_user is passed in, the credentials will be linked to the passed
|
||||
in user if the credentials are new.
|
||||
"""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
# New credentials, store in user
|
||||
if credentials.is_new:
|
||||
info = await auth_provider.async_user_meta_for_credentials(
|
||||
credentials)
|
||||
# Make owner and activate user if it's the first user.
|
||||
if self.users:
|
||||
is_owner = False
|
||||
is_active = False
|
||||
else:
|
||||
is_owner = True
|
||||
is_active = True
|
||||
|
||||
new_user = User(
|
||||
is_owner=is_owner,
|
||||
is_active=is_active,
|
||||
name=info.get('name'),
|
||||
)
|
||||
self.users[new_user.id] = new_user
|
||||
await self.async_link_user(new_user, credentials)
|
||||
return new_user
|
||||
|
||||
for user in self.users.values():
|
||||
for creds in user.credentials:
|
||||
if (creds.auth_provider_type == credentials.auth_provider_type
|
||||
and creds.auth_provider_id ==
|
||||
credentials.auth_provider_id):
|
||||
return user
|
||||
|
||||
raise ValueError('We got credentials with ID but found no user')
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
"""Add credentials to an existing user."""
|
||||
user.credentials.append(credentials)
|
||||
await self.async_save()
|
||||
credentials.is_new = False
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
"""Remove a user."""
|
||||
self.users.pop(user.id)
|
||||
await self.async_save()
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id):
|
||||
"""Create a new token for a user."""
|
||||
refresh_token = RefreshToken(user, client_id)
|
||||
user.refresh_tokens[refresh_token.token] = refresh_token
|
||||
await self.async_save()
|
||||
return refresh_token
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
if self.users is None:
|
||||
await self.async_load()
|
||||
|
||||
for user in self.users.values():
|
||||
refresh_token = user.refresh_tokens.get(token)
|
||||
if refresh_token is not None:
|
||||
return refresh_token
|
||||
|
||||
return None
|
||||
|
||||
async def async_create_client(self, name, redirect_uris, no_secret):
|
||||
"""Create a new client."""
|
||||
if self.clients is None:
|
||||
await self.async_load()
|
||||
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'redirect_uris': redirect_uris
|
||||
}
|
||||
|
||||
if no_secret:
|
||||
kwargs['secret'] = None
|
||||
|
||||
client = Client(**kwargs)
|
||||
self.clients[client.id] = client
|
||||
await self.async_save()
|
||||
return client
|
||||
|
||||
async def async_get_client(self, client_id):
|
||||
"""Get a client."""
|
||||
if self.clients is None:
|
||||
await self.async_load()
|
||||
|
||||
return self.clients.get(client_id)
|
||||
|
||||
async def async_load(self):
|
||||
"""Load the users."""
|
||||
async with self._load_lock:
|
||||
self.users = {}
|
||||
self.clients = {}
|
||||
|
||||
async def async_save(self):
|
||||
"""Save users."""
|
||||
pass
|
||||
1
homeassistant/auth_providers/__init__.py
Normal file
1
homeassistant/auth_providers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Auth providers for Home Assistant."""
|
||||
181
homeassistant/auth_providers/homeassistant.py
Normal file
181
homeassistant/auth_providers/homeassistant.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Home Assistant auth provider."""
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import auth, data_entry_flow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import json
|
||||
|
||||
|
||||
PATH_DATA = '.users.json'
|
||||
|
||||
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Raised when we encounter invalid authentication."""
|
||||
|
||||
|
||||
class InvalidUser(HomeAssistantError):
|
||||
"""Raised when invalid user is specified.
|
||||
|
||||
Will not be raised when validating authentication.
|
||||
"""
|
||||
|
||||
|
||||
class Data:
|
||||
"""Hold the user data."""
|
||||
|
||||
def __init__(self, path, data):
|
||||
"""Initialize the user data store."""
|
||||
self.path = path
|
||||
if data is None:
|
||||
data = {
|
||||
'salt': auth.generate_secret(),
|
||||
'users': []
|
||||
}
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
"""Return users."""
|
||||
return self._data['users']
|
||||
|
||||
def validate_login(self, username, password):
|
||||
"""Validate a username and password.
|
||||
|
||||
Raises InvalidAuth if auth invalid.
|
||||
"""
|
||||
password = self.hash_password(password)
|
||||
|
||||
found = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
for user in self._data['users']:
|
||||
if username == user['username']:
|
||||
found = user
|
||||
|
||||
if found is None:
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(password, password)
|
||||
raise InvalidAuth
|
||||
|
||||
if not hmac.compare_digest(password,
|
||||
base64.b64decode(found['password'])):
|
||||
raise InvalidAuth
|
||||
|
||||
def hash_password(self, password, for_storage=False):
|
||||
"""Encode a password."""
|
||||
hashed = hashlib.pbkdf2_hmac(
|
||||
'sha512', password.encode(), self._data['salt'].encode(), 100000)
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed).decode()
|
||||
return hashed
|
||||
|
||||
def add_user(self, username, password):
|
||||
"""Add a user."""
|
||||
if any(user['username'] == username for user in self.users):
|
||||
raise InvalidUser
|
||||
|
||||
self.users.append({
|
||||
'username': username,
|
||||
'password': self.hash_password(password, True),
|
||||
})
|
||||
|
||||
def change_password(self, username, new_password):
|
||||
"""Update the password of a user.
|
||||
|
||||
Raises InvalidUser if user cannot be found.
|
||||
"""
|
||||
for user in self.users:
|
||||
if user['username'] == username:
|
||||
user['password'] = self.hash_password(new_password, True)
|
||||
break
|
||||
else:
|
||||
raise InvalidUser
|
||||
|
||||
def save(self):
|
||||
"""Save data."""
|
||||
json.save_json(self.path, self._data)
|
||||
|
||||
|
||||
def load_data(path):
|
||||
"""Load auth data."""
|
||||
return Data(path, json.load_json(path, None))
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('homeassistant')
|
||||
class HassAuthProvider(auth.AuthProvider):
|
||||
"""Auth provider based on a local storage of users in HASS config dir."""
|
||||
|
||||
DEFAULT_TITLE = 'Home Assistant Local'
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
|
||||
async def async_validate_login(self, username, password):
|
||||
"""Helper to validate a username and password."""
|
||||
def validate():
|
||||
"""Validate creds."""
|
||||
data = self._auth_data()
|
||||
data.validate_login(username, password)
|
||||
|
||||
await self.hass.async_add_job(validate)
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
return credential
|
||||
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({
|
||||
'username': username
|
||||
})
|
||||
|
||||
def _auth_data(self):
|
||||
"""Return the auth provider data."""
|
||||
return load_data(self.hass.config.path(PATH_DATA))
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider):
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self._auth_provider.async_validate_login(
|
||||
user_input['username'], user_input['password'])
|
||||
except InvalidAuth:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data=user_input
|
||||
)
|
||||
|
||||
schema = OrderedDict()
|
||||
schema['username'] = str
|
||||
schema['password'] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
)
|
||||
118
homeassistant/auth_providers/insecure_example.py
Normal file
118
homeassistant/auth_providers/insecure_example.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Example auth provider."""
|
||||
from collections import OrderedDict
|
||||
import hmac
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import auth, data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
vol.Required('username'): str,
|
||||
vol.Required('password'): str,
|
||||
vol.Optional('name'): str,
|
||||
})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
vol.Required('users'): [USER_SCHEMA]
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('insecure_example')
|
||||
class ExampleAuthProvider(auth.AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
|
||||
@callback
|
||||
def async_validate_login(self, username, password):
|
||||
"""Helper to validate a username and password."""
|
||||
user = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
for usr in self.config['users']:
|
||||
if hmac.compare_digest(username.encode('utf-8'),
|
||||
usr['username'].encode('utf-8')):
|
||||
user = usr
|
||||
|
||||
if user is None:
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(password.encode('utf-8'),
|
||||
password.encode('utf-8'))
|
||||
raise InvalidAuthError
|
||||
|
||||
if not hmac.compare_digest(user['password'].encode('utf-8'),
|
||||
password.encode('utf-8')):
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
return credential
|
||||
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({
|
||||
'username': username
|
||||
})
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
username = credentials.data['username']
|
||||
|
||||
for user in self.config['users']:
|
||||
if user['username'] == username:
|
||||
return {
|
||||
'name': user.get('name')
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider):
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
self._auth_provider.async_validate_login(
|
||||
user_input['username'], user_input['password'])
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data=user_input
|
||||
)
|
||||
|
||||
schema = OrderedDict()
|
||||
schema['username'] = str
|
||||
schema['password'] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -12,8 +12,7 @@ from typing import Any, Optional, Dict
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import (
|
||||
core, config as conf_util, config_entries, loader,
|
||||
components as core_components)
|
||||
core, config as conf_util, config_entries, components as core_components)
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -42,7 +41,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,21 +60,21 @@ 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
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_from_config_dict(config: Dict[str, Any],
|
||||
hass: core.HomeAssistant,
|
||||
config_dir: Optional[str] = None,
|
||||
enable_log: bool = True,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = False,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None) \
|
||||
async def async_from_config_dict(config: Dict[str, Any],
|
||||
hass: core.HomeAssistant,
|
||||
config_dir: Optional[str] = None,
|
||||
enable_log: bool = True,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = False,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
@@ -84,32 +84,30 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
start = time()
|
||||
|
||||
if enable_log:
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
yield from conf_util.async_process_ha_core_config(hass, core_config)
|
||||
await conf_util.async_process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
||||
await hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
_LOGGER.warning("Skipping pip installation of required modules. "
|
||||
"This may cause issues")
|
||||
|
||||
if not loader.PREPARED:
|
||||
yield from hass.async_add_job(loader.prepare, hass)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
config = OrderedDict(config)
|
||||
|
||||
# Merge packages
|
||||
conf_util.merge_packages_config(
|
||||
config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||
|
||||
# Ensure we have no None values after merge
|
||||
for key, value in config.items():
|
||||
@@ -117,7 +115,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
config[key] = {}
|
||||
|
||||
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
||||
yield from hass.config_entries.async_load()
|
||||
await hass.config_entries.async_load()
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
@@ -126,13 +124,13 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
|
||||
# setup components
|
||||
# pylint: disable=not-an-iterable
|
||||
res = yield from core_components.async_setup(hass, config)
|
||||
res = await core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"further initialization aborted")
|
||||
return hass
|
||||
|
||||
yield from persistent_notification.async_setup(hass, config)
|
||||
await persistent_notification.async_setup(hass, config)
|
||||
|
||||
_LOGGER.info("Home Assistant core initialized")
|
||||
|
||||
@@ -142,7 +140,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
|
||||
yield from hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# stage 2
|
||||
for component in components:
|
||||
@@ -150,7 +148,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
|
||||
yield from hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
stop = time()
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||
@@ -164,7 +162,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,
|
||||
@@ -176,19 +175,20 @@ 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
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_from_config_file(config_path: str,
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None):
|
||||
async def async_from_config_file(config_path: str,
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: 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.
|
||||
@@ -197,12 +197,13 @@ def async_from_config_file(config_path: str,
|
||||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
yield from async_mount_local_lib_path(config_dir, hass.loop)
|
||||
await 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(
|
||||
config_dict = await hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Error loading %s: %s", config_path, err)
|
||||
@@ -210,46 +211,57 @@ def async_from_config_file(config_path: str,
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
|
||||
hass = yield from async_from_config_dict(
|
||||
hass = await async_from_config_dict(
|
||||
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
||||
return hass
|
||||
|
||||
|
||||
@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)
|
||||
@@ -266,7 +278,8 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
|
||||
|
||||
if log_rotate_days:
|
||||
err_handler = logging.handlers.TimedRotatingFileHandler(
|
||||
err_log_path, when='midnight', backupCount=log_rotate_days)
|
||||
err_log_path, when='midnight',
|
||||
backupCount=log_rotate_days) # type: logging.FileHandler
|
||||
else:
|
||||
err_handler = logging.FileHandler(
|
||||
err_log_path, mode='w', delay=True)
|
||||
@@ -276,17 +289,16 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
|
||||
|
||||
async_handler = AsyncHandler(hass.loop, err_handler)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_async_handler(event):
|
||||
async def async_stop_async_handler(event):
|
||||
"""Cleanup async handler."""
|
||||
logging.getLogger('').removeHandler(async_handler)
|
||||
yield from async_handler.async_close(blocking=True)
|
||||
await async_handler.async_close(blocking=True)
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(async_handler)
|
||||
logger.addHandler(async_handler) # type: ignore
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Save the log file location for access by other components.
|
||||
@@ -305,15 +317,14 @@ def mount_local_lib_path(config_dir: str) -> str:
|
||||
return deps_dir
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_mount_local_lib_path(config_dir: str,
|
||||
loop: asyncio.AbstractEventLoop) -> str:
|
||||
async def async_mount_local_lib_path(config_dir: str,
|
||||
loop: asyncio.AbstractEventLoop) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
lib_dir = yield from async_get_user_site(deps_dir, loop=loop)
|
||||
lib_dir = await async_get_user_site(deps_dir, loop=loop)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
@@ -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.3']
|
||||
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'
|
||||
@@ -80,19 +81,20 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
|
||||
ABODE_PLATFORMS = [
|
||||
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover',
|
||||
'camera', 'light'
|
||||
'camera', 'light', 'sensor'
|
||||
]
|
||||
|
||||
|
||||
class AbodeSystem(object):
|
||||
"""Abode System class."""
|
||||
|
||||
def __init__(self, username, password, name, polling, exclude, lights):
|
||||
def __init__(self, username, password, cache,
|
||||
name, polling, exclude, lights):
|
||||
"""Initialize the system."""
|
||||
import abodepy
|
||||
self.abode = abodepy.Abode(
|
||||
username, password, auto_login=True, get_devices=True,
|
||||
get_automations=True)
|
||||
get_automations=True, cache_path=cache)
|
||||
self.name = name
|
||||
self.polling = polling
|
||||
self.exclude = exclude
|
||||
@@ -129,8 +131,9 @@ def setup(hass, config):
|
||||
lights = conf.get(CONF_LIGHTS)
|
||||
|
||||
try:
|
||||
cache = hass.config.path(DEFAULT_CACHEDB)
|
||||
hass.data[DOMAIN] = AbodeSystem(
|
||||
username, password, name, polling, exclude, lights)
|
||||
username, password, cache, name, polling, exclude, lights)
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.1']
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -93,6 +93,13 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'sensor_status': self._alarm.sensor_status
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -1471,6 +1471,7 @@ async def async_api_adjust_target_temp(hass, config, request, 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
|
||||
|
||||
@@ -76,8 +76,7 @@ class APIEventStream(HomeAssistantView):
|
||||
url = URL_API_STREAM
|
||||
name = "api:stream"
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
# pylint: disable=no-self-use
|
||||
hass = request.app['hass']
|
||||
@@ -88,8 +87,7 @@ class APIEventStream(HomeAssistantView):
|
||||
if restrict:
|
||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||
|
||||
@asyncio.coroutine
|
||||
def forward_events(event):
|
||||
async def forward_events(event):
|
||||
"""Forward events to the open request."""
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
@@ -104,11 +102,11 @@ class APIEventStream(HomeAssistantView):
|
||||
else:
|
||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||
|
||||
yield from to_write.put(data)
|
||||
await to_write.put(data)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = 'text/event-stream'
|
||||
yield from response.prepare(request)
|
||||
await response.prepare(request)
|
||||
|
||||
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||
|
||||
@@ -116,13 +114,13 @@ class APIEventStream(HomeAssistantView):
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
|
||||
# Fire off one message so browsers fire open event right away
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
await to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
while True:
|
||||
try:
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||
loop=hass.loop):
|
||||
payload = yield from to_write.get()
|
||||
payload = await to_write.get()
|
||||
|
||||
if payload is stop_obj:
|
||||
break
|
||||
@@ -130,9 +128,9 @@ class APIEventStream(HomeAssistantView):
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
yield from response.write(msg.encode("UTF-8"))
|
||||
await response.write(msg.encode("UTF-8"))
|
||||
except asyncio.TimeoutError:
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
await to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug('STREAM %s ABORT', id(stop_obj))
|
||||
@@ -200,12 +198,11 @@ class APIEntityStateView(HomeAssistantView):
|
||||
return self.json(state)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
async def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
data = await request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
@@ -257,10 +254,9 @@ class APIEventView(HomeAssistantView):
|
||||
url = '/api/events/{event_type}'
|
||||
name = "api:event"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, event_type):
|
||||
async def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
body = yield from request.text()
|
||||
body = await request.text()
|
||||
try:
|
||||
event_data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
@@ -292,10 +288,9 @@ class APIServicesView(HomeAssistantView):
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
"""Get registered services."""
|
||||
services = yield from async_services_json(request.app['hass'])
|
||||
services = await async_services_json(request.app['hass'])
|
||||
return self.json(services)
|
||||
|
||||
|
||||
@@ -305,14 +300,13 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
url = "/api/services/{domain}/{service}"
|
||||
name = "api:domain-services"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, domain, service):
|
||||
async def post(self, request, domain, service):
|
||||
"""Call a service.
|
||||
|
||||
Returns a list of changed states.
|
||||
"""
|
||||
hass = request.app['hass']
|
||||
body = yield from request.text()
|
||||
body = await request.text()
|
||||
try:
|
||||
data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
@@ -320,7 +314,7 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
yield from hass.services.async_call(domain, service, data, True)
|
||||
await hass.services.async_call(domain, service, data, True)
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
@@ -343,11 +337,10 @@ class APITemplateView(HomeAssistantView):
|
||||
url = URL_API_TEMPLATE
|
||||
name = "api:template"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
"""Render a template."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
data = await request.json()
|
||||
tpl = template.Template(data['template'], request.app['hass'])
|
||||
return tpl.async_render(data.get('variables'))
|
||||
except (ValueError, TemplateError) as ex:
|
||||
@@ -363,13 +356,13 @@ class APIErrorLog(HomeAssistantView):
|
||||
|
||||
async def get(self, request):
|
||||
"""Retrieve API error log."""
|
||||
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
|
||||
return web.FileResponse(
|
||||
request.app['hass'].data[DATA_LOGGING])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_services_json(hass):
|
||||
async def async_services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
descriptions = yield from async_get_all_descriptions(hass)
|
||||
descriptions = await async_get_all_descriptions(hass)
|
||||
return [{"domain": key, "services": value}
|
||||
for key, value in descriptions.items()]
|
||||
|
||||
|
||||
351
homeassistant/components/auth/__init__.py
Normal file
351
homeassistant/components/auth/__init__.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""Component to allow users to login and get tokens.
|
||||
|
||||
All requests will require passing in a valid client ID and secret via HTTP
|
||||
Basic Auth.
|
||||
|
||||
# GET /auth/providers
|
||||
|
||||
Return a list of auth providers. Example:
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Local",
|
||||
"id": null,
|
||||
"type": "local_provider",
|
||||
}
|
||||
]
|
||||
|
||||
# POST /auth/login_flow
|
||||
|
||||
Create a login flow. Will return the first step of the flow.
|
||||
|
||||
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
|
||||
are identified by type and id.
|
||||
|
||||
{
|
||||
"handler": ["local_provider", null]
|
||||
}
|
||||
|
||||
Return value will be a step in a data entry flow. See the docs for data entry
|
||||
flow for details.
|
||||
|
||||
{
|
||||
"data_schema": [
|
||||
{"name": "username", "type": "string"},
|
||||
{"name": "password", "type": "string"}
|
||||
],
|
||||
"errors": {},
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"step_id": "init",
|
||||
"type": "form"
|
||||
}
|
||||
|
||||
# POST /auth/login_flow/{flow_id}
|
||||
|
||||
Progress the flow. Most flows will be 1 page, but could optionally add extra
|
||||
login challenges, like TFA. Once the flow has finished, the returned step will
|
||||
have type "create_entry" and "result" key will contain an authorization code.
|
||||
|
||||
{
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"result": "411ee2f916e648d691e937ae9344681e",
|
||||
"source": "user",
|
||||
"title": "Example",
|
||||
"type": "create_entry",
|
||||
"version": 1
|
||||
}
|
||||
|
||||
# POST /auth/token
|
||||
|
||||
This is an OAuth2 endpoint for granting tokens. We currently support the grant
|
||||
types "authorization_code" and "refresh_token". Because we follow the OAuth2
|
||||
spec, data should be send in formatted as x-www-form-urlencoded. Examples will
|
||||
be in JSON as it's more readable.
|
||||
|
||||
## Grant type authorization_code
|
||||
|
||||
Exchange the authorization code retrieved from the login flow for tokens.
|
||||
|
||||
{
|
||||
"grant_type": "authorization_code",
|
||||
"code": "411ee2f916e648d691e937ae9344681e"
|
||||
}
|
||||
|
||||
Return value will be the access and refresh tokens. The access token will have
|
||||
a limited expiration. New access tokens can be requested using the refresh
|
||||
token.
|
||||
|
||||
{
|
||||
"access_token": "ABCDEFGH",
|
||||
"expires_in": 1800,
|
||||
"refresh_token": "IJKLMNOPQRST",
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
|
||||
## Grant type refresh_token
|
||||
|
||||
Request a new access token using a refresh token.
|
||||
|
||||
{
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": "IJKLMNOPQRST"
|
||||
}
|
||||
|
||||
Return value will be a new access token. The access token will have
|
||||
a limited expiration.
|
||||
|
||||
{
|
||||
"access_token": "ABCDEFGH",
|
||||
"expires_in": 1800,
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import aiohttp.web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
|
||||
from .client import verify_client
|
||||
|
||||
DOMAIN = 'auth'
|
||||
DEPENDENCIES = ['http']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Component to allow users to login."""
|
||||
store_credentials, retrieve_credentials = _create_cred_store()
|
||||
|
||||
hass.http.register_view(AuthProvidersView)
|
||||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
|
||||
hass.http.register_view(
|
||||
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
|
||||
hass.http.register_view(GrantTokenView(retrieve_credentials))
|
||||
hass.http.register_view(LinkUserView(retrieve_credentials))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AuthProvidersView(HomeAssistantView):
|
||||
"""View to get available auth providers."""
|
||||
|
||||
url = '/auth/providers'
|
||||
name = 'api:auth:providers'
|
||||
requires_auth = False
|
||||
|
||||
@verify_client
|
||||
async def get(self, request, client):
|
||||
"""Get available auth providers."""
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in request.app['hass'].auth.async_auth_providers])
|
||||
|
||||
|
||||
class LoginFlowIndexView(FlowManagerIndexView):
|
||||
"""View to create a config flow."""
|
||||
|
||||
url = '/auth/login_flow'
|
||||
name = 'api:auth:login_flow'
|
||||
requires_auth = False
|
||||
|
||||
async def get(self, request):
|
||||
"""Do not allow index of flows in progress."""
|
||||
return aiohttp.web.Response(status=405)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@verify_client
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('handler'): vol.Any(str, list),
|
||||
vol.Required('redirect_uri'): str,
|
||||
}))
|
||||
async def post(self, request, client, data):
|
||||
"""Create a new login flow."""
|
||||
if data['redirect_uri'] not in client.redirect_uris:
|
||||
return self.json_message('invalid redirect uri', )
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request)
|
||||
|
||||
|
||||
class LoginFlowResourceView(FlowManagerResourceView):
|
||||
"""View to interact with the flow manager."""
|
||||
|
||||
url = '/auth/login_flow/{flow_id}'
|
||||
name = 'api:auth:login_flow:resource'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, flow_mgr, store_credentials):
|
||||
"""Initialize the login flow resource view."""
|
||||
super().__init__(flow_mgr)
|
||||
self._store_credentials = store_credentials
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
async def get(self, request):
|
||||
"""Do not allow getting status of a flow in progress."""
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@verify_client
|
||||
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
|
||||
async def post(self, request, client, flow_id, data):
|
||||
"""Handle progressing a login flow request."""
|
||||
try:
|
||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
except vol.Invalid:
|
||||
return self.json_message('User input malformed', 400)
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return self.json(self._prepare_result_json(result))
|
||||
|
||||
result.pop('data')
|
||||
result['result'] = self._store_credentials(client.id, result['result'])
|
||||
|
||||
return self.json(result)
|
||||
|
||||
|
||||
class GrantTokenView(HomeAssistantView):
|
||||
"""View to grant tokens."""
|
||||
|
||||
url = '/auth/token'
|
||||
name = 'api:auth:token'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, retrieve_credentials):
|
||||
"""Initialize the grant token view."""
|
||||
self._retrieve_credentials = retrieve_credentials
|
||||
|
||||
@verify_client
|
||||
async def post(self, request, client):
|
||||
"""Grant a token."""
|
||||
hass = request.app['hass']
|
||||
data = await request.post()
|
||||
grant_type = data.get('grant_type')
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
return await self._async_handle_auth_code(
|
||||
hass, client.id, data)
|
||||
|
||||
elif grant_type == 'refresh_token':
|
||||
return await self._async_handle_refresh_token(
|
||||
hass, client.id, data)
|
||||
|
||||
return self.json({
|
||||
'error': 'unsupported_grant_type',
|
||||
}, status_code=400)
|
||||
|
||||
async def _async_handle_auth_code(self, hass, client_id, data):
|
||||
"""Handle authorization code request."""
|
||||
code = data.get('code')
|
||||
|
||||
if code is None:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
}, status_code=400)
|
||||
|
||||
credentials = self._retrieve_credentials(client_id, code)
|
||||
|
||||
if credentials is None:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
}, status_code=400)
|
||||
|
||||
user = await hass.auth.async_get_or_create_user(credentials)
|
||||
refresh_token = await hass.auth.async_create_refresh_token(user,
|
||||
client_id)
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
|
||||
return self.json({
|
||||
'access_token': access_token.token,
|
||||
'token_type': 'Bearer',
|
||||
'refresh_token': refresh_token.token,
|
||||
'expires_in':
|
||||
int(refresh_token.access_token_expiration.total_seconds()),
|
||||
})
|
||||
|
||||
async def _async_handle_refresh_token(self, hass, client_id, data):
|
||||
"""Handle authorization code request."""
|
||||
token = data.get('refresh_token')
|
||||
|
||||
if token is None:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
}, status_code=400)
|
||||
|
||||
refresh_token = await hass.auth.async_get_refresh_token(token)
|
||||
|
||||
if refresh_token is None or refresh_token.client_id != client_id:
|
||||
return self.json({
|
||||
'error': 'invalid_grant',
|
||||
}, status_code=400)
|
||||
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
|
||||
return self.json({
|
||||
'access_token': access_token.token,
|
||||
'token_type': 'Bearer',
|
||||
'expires_in':
|
||||
int(refresh_token.access_token_expiration.total_seconds()),
|
||||
})
|
||||
|
||||
|
||||
class LinkUserView(HomeAssistantView):
|
||||
"""View to link existing users to new credentials."""
|
||||
|
||||
url = '/auth/link_user'
|
||||
name = 'api:auth:link_user'
|
||||
|
||||
def __init__(self, retrieve_credentials):
|
||||
"""Initialize the link user view."""
|
||||
self._retrieve_credentials = retrieve_credentials
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
'code': str,
|
||||
'client_id': str,
|
||||
}))
|
||||
async def post(self, request, data):
|
||||
"""Link a user."""
|
||||
hass = request.app['hass']
|
||||
user = request['hass_user']
|
||||
|
||||
credentials = self._retrieve_credentials(
|
||||
data['client_id'], data['code'])
|
||||
|
||||
if credentials is None:
|
||||
return self.json_message('Invalid code', status_code=400)
|
||||
|
||||
await hass.auth.async_link_user(user, credentials)
|
||||
return self.json_message('User linked')
|
||||
|
||||
|
||||
@callback
|
||||
def _create_cred_store():
|
||||
"""Create a credential store."""
|
||||
temp_credentials = {}
|
||||
|
||||
@callback
|
||||
def store_credentials(client_id, credentials):
|
||||
"""Store credentials and return a code to retrieve it."""
|
||||
code = uuid.uuid4().hex
|
||||
temp_credentials[(client_id, code)] = credentials
|
||||
return code
|
||||
|
||||
@callback
|
||||
def retrieve_credentials(client_id, code):
|
||||
"""Retrieve credentials."""
|
||||
return temp_credentials.pop((client_id, code), None)
|
||||
|
||||
return store_credentials, retrieve_credentials
|
||||
79
homeassistant/components/auth/client.py
Normal file
79
homeassistant/components/auth/client.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Helpers to resolve client ID/secret."""
|
||||
import base64
|
||||
from functools import wraps
|
||||
import hmac
|
||||
|
||||
import aiohttp.hdrs
|
||||
|
||||
|
||||
def verify_client(method):
|
||||
"""Decorator to verify client id/secret on requests."""
|
||||
@wraps(method)
|
||||
async def wrapper(view, request, *args, **kwargs):
|
||||
"""Verify client id/secret before doing request."""
|
||||
client = await _verify_client(request)
|
||||
|
||||
if client is None:
|
||||
return view.json({
|
||||
'error': 'invalid_client',
|
||||
}, status_code=401)
|
||||
|
||||
return await method(
|
||||
view, request, *args, **kwargs, client=client)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
async def _verify_client(request):
|
||||
"""Method to verify the client id/secret in consistent time.
|
||||
|
||||
By using a consistent time for looking up client id and comparing the
|
||||
secret, we prevent attacks by malicious actors trying different client ids
|
||||
and are able to derive from the time it takes to process the request if
|
||||
they guessed the client id correctly.
|
||||
"""
|
||||
if aiohttp.hdrs.AUTHORIZATION not in request.headers:
|
||||
return None
|
||||
|
||||
auth_type, auth_value = \
|
||||
request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1)
|
||||
|
||||
if auth_type != 'Basic':
|
||||
return None
|
||||
|
||||
decoded = base64.b64decode(auth_value).decode('utf-8')
|
||||
try:
|
||||
client_id, client_secret = decoded.split(':', 1)
|
||||
except ValueError:
|
||||
# If no ':' in decoded
|
||||
client_id, client_secret = decoded, None
|
||||
|
||||
return await async_secure_get_client(
|
||||
request.app['hass'], client_id, client_secret)
|
||||
|
||||
|
||||
async def async_secure_get_client(hass, client_id, client_secret):
|
||||
"""Get a client id/secret in consistent time."""
|
||||
client = await hass.auth.async_get_client(client_id)
|
||||
|
||||
if client is None:
|
||||
if client_secret is not None:
|
||||
# Still do a compare so we run same time as if a client was found.
|
||||
hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client_secret.encode('utf-8'))
|
||||
return None
|
||||
|
||||
if client.secret is None:
|
||||
return client
|
||||
|
||||
elif client_secret is None:
|
||||
# Still do a compare so we run same time as if a secret was passed.
|
||||
hmac.compare_digest(client.secret.encode('utf-8'),
|
||||
client.secret.encode('utf-8'))
|
||||
return None
|
||||
|
||||
elif hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client.secret.encode('utf-8')):
|
||||
return client
|
||||
|
||||
return None
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/automation/
|
||||
"""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -22,7 +23,6 @@ from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.loader import get_platform
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -58,12 +58,14 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def _platform_validator(config):
|
||||
"""Validate it is a valid platform."""
|
||||
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
|
||||
try:
|
||||
platform = importlib.import_module(
|
||||
'homeassistant.components.automation.{}'.format(
|
||||
config[CONF_PLATFORM]))
|
||||
except ImportError:
|
||||
raise vol.Invalid('Invalid platform specified') from None
|
||||
|
||||
if not hasattr(platform, 'TRIGGER_SCHEMA'):
|
||||
return config
|
||||
|
||||
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
||||
return platform.TRIGGER_SCHEMA(config)
|
||||
|
||||
|
||||
_TRIGGER_SCHEMA = vol.All(
|
||||
@@ -71,7 +73,7 @@ _TRIGGER_SCHEMA = vol.All(
|
||||
[
|
||||
vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
|
||||
vol.Required(CONF_PLATFORM): str
|
||||
}, extra=vol.ALLOW_EXTRA),
|
||||
_platform_validator
|
||||
),
|
||||
|
||||
@@ -50,13 +50,23 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = EntityComponent(
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Setup a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class BinarySensorDevice(Entity):
|
||||
"""Represent a binary sensor."""
|
||||
|
||||
@@ -217,4 +217,4 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
self._deviation = bool(self.probability > self._probability_threshold)
|
||||
self._deviation = bool(self.probability >= self._probability_threshold)
|
||||
|
||||
@@ -11,7 +11,6 @@ import voluptuous as vol
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -31,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the available BloomSky weather binary sensors."""
|
||||
bloomsky = get_component('bloomsky')
|
||||
bloomsky = hass.components.bloomsky
|
||||
# Default needed in case of discovery
|
||||
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES)
|
||||
|
||||
|
||||
@@ -17,9 +17,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {
|
||||
'lids': ['Doors', 'opening'],
|
||||
'windows': ['Windows', 'opening'],
|
||||
'door_lock_state': ['Door lock state', 'safety']
|
||||
'door_lock_state': ['Door lock state', 'safety'],
|
||||
'lights_parking': ['Parking lights', 'light'],
|
||||
'condition_based_services': ['Condition based services', 'problem'],
|
||||
'check_control_messages': ['Control messages', 'problem']
|
||||
}
|
||||
|
||||
SENSOR_TYPES_ELEC = {
|
||||
'charging_status': ['Charging status', 'power'],
|
||||
'connection_status': ['Connection status', 'plug']
|
||||
}
|
||||
|
||||
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the BMW sensors."""
|
||||
@@ -29,10 +39,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
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)
|
||||
if vehicle.has_hv_battery:
|
||||
_LOGGER.debug('BMW with a high voltage battery')
|
||||
for key, value in sorted(SENSOR_TYPES_ELEC.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
elif vehicle.has_internal_combustion_engine:
|
||||
_LOGGER.debug('BMW with an internal combustion engine')
|
||||
for key, value in sorted(SENSOR_TYPES.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
@@ -46,6 +64,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
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
|
||||
@@ -55,6 +74,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
"""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."""
|
||||
@@ -86,12 +110,34 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
result[window.name] = window.state.value
|
||||
elif self._attribute == 'door_lock_state':
|
||||
result['door_lock_state'] = vehicle_state.door_lock_state.value
|
||||
result['last_update_reason'] = vehicle_state.last_update_reason
|
||||
elif self._attribute == 'lights_parking':
|
||||
result['lights_parking'] = vehicle_state.parking_lights.value
|
||||
elif self._attribute == 'condition_based_services':
|
||||
for report in vehicle_state.condition_based_services:
|
||||
result.update(self._format_cbs_report(report))
|
||||
elif self._attribute == 'check_control_messages':
|
||||
check_control_messages = vehicle_state.check_control_messages
|
||||
if not check_control_messages:
|
||||
result['check_control_messages'] = 'OK'
|
||||
else:
|
||||
result['check_control_messages'] = check_control_messages
|
||||
elif self._attribute == 'charging_status':
|
||||
result['charging_status'] = vehicle_state.charging_status.value
|
||||
# pylint: disable=W0212
|
||||
result['last_charging_end_result'] = \
|
||||
vehicle_state._attributes['lastChargingEndResult']
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=W0212
|
||||
result['connection_status'] = \
|
||||
vehicle_state._attributes['connectionStatus']
|
||||
|
||||
return result
|
||||
return sorted(result.items())
|
||||
|
||||
def update(self):
|
||||
"""Read new state data from the library."""
|
||||
from bimmer_connected.state import LockState
|
||||
from bimmer_connected.state import ChargingState
|
||||
vehicle_state = self._vehicle.state
|
||||
|
||||
# device class opening: On means open, Off means closed
|
||||
@@ -105,6 +151,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._state = vehicle_state.door_lock_state not in \
|
||||
[LockState.LOCKED, LockState.SECURED]
|
||||
# device class light: On means light detected, Off means no light
|
||||
if self._attribute == 'lights_parking':
|
||||
self._state = vehicle_state.are_parking_lights_on
|
||||
# device class problem: On means problem detected, Off means no problem
|
||||
if self._attribute == 'condition_based_services':
|
||||
self._state = not vehicle_state.are_all_cbs_ok
|
||||
if self._attribute == 'check_control_messages':
|
||||
self._state = vehicle_state.has_check_control_messages
|
||||
# device class power: On means power detected, Off means no power
|
||||
if self._attribute == 'charging_status':
|
||||
self._state = vehicle_state.charging_status in \
|
||||
[ChargingState.CHARGING]
|
||||
# device class plug: On means device is plugged in,
|
||||
# Off means device is unplugged
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=W0212
|
||||
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
||||
'CONNECTED')
|
||||
|
||||
@staticmethod
|
||||
def _format_cbs_report(report):
|
||||
result = {}
|
||||
service_type = report.service_type.lower().replace('_', ' ')
|
||||
result['{} status'.format(service_type)] = report.state.value
|
||||
if report.due_date is not None:
|
||||
result['{} date'.format(service_type)] = \
|
||||
report.due_date.strftime('%Y-%m-%d')
|
||||
if report.due_distance is not None:
|
||||
result['{} distance'.format(service_type)] = \
|
||||
'{} km'.format(report.due_distance)
|
||||
return result
|
||||
|
||||
def update_callback(self):
|
||||
"""Schedule a state update."""
|
||||
|
||||
@@ -6,27 +6,35 @@ https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import (
|
||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
|
||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['deconz']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Old way of setting up deCONZ binary sensors."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@callback
|
||||
def async_add_sensor(sensors):
|
||||
"""Add binary sensor from deCONZ."""
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
entities = []
|
||||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_BINARY_SENSOR:
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
|
||||
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
sensors = hass.data[DATA_DECONZ].sensors
|
||||
entities = []
|
||||
|
||||
for sensor in sensors.values():
|
||||
if sensor and sensor.type in DECONZ_BINARY_SENSOR:
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
|
||||
|
||||
|
||||
class DeconzBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -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)
|
||||
|
||||
85
homeassistant/components/binary_sensor/homematicip_cloud.py
Normal file
85
homeassistant/components/binary_sensor/homematicip_cloud.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Support for HomematicIP binary sensor.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_WINDOW_STATE = 'window_state'
|
||||
ATTR_EVENT_DELAY = 'event_delay'
|
||||
ATTR_MOTION_DETECTED = 'motion_detected'
|
||||
ATTR_ILLUMINATION = 'illumination'
|
||||
|
||||
HMIP_OPEN = 'open'
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP binary sensor devices."""
|
||||
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, ShutterContact):
|
||||
devices.append(HomematicipShutterContact(home, device))
|
||||
elif isinstance(device, MotionDetectorIndoor):
|
||||
devices.append(HomematicipMotionDetector(home, device))
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""HomematicIP shutter contact."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return 'door'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the shutter contact is on/open."""
|
||||
if self._device.sabotage:
|
||||
return True
|
||||
if self._device.windowState is None:
|
||||
return None
|
||||
return self._device.windowState.lower() == HMIP_OPEN
|
||||
|
||||
|
||||
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""MomematicIP motion detector."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return 'motion'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if motion is detected."""
|
||||
if self._device.sabotage:
|
||||
return True
|
||||
return self._device.motionDetected
|
||||
@@ -23,7 +23,7 @@ SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the INSTEON PLM device class for the hass platform."""
|
||||
plm = hass.data['insteon_plm']
|
||||
plm = hass.data['insteon_plm'].get('plm')
|
||||
|
||||
address = discovery_info['address']
|
||||
device = plm.devices[address]
|
||||
|
||||
@@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
# pylint: disable=protected-access
|
||||
if _is_val_unknown(self._node.status._val):
|
||||
self._computed_state = None
|
||||
self._status_was_unknown = True
|
||||
else:
|
||||
self._computed_state = bool(self._node.status._val)
|
||||
self._status_was_unknown = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
@@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
# pylint: disable=protected-access
|
||||
if not _is_val_unknown(self._negative_node.status._val):
|
||||
# If the negative node has a value, it means the negative node is
|
||||
# in use for this device. Therefore, we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
# in use for this device. Next we need to check to see if the
|
||||
# negative and positive nodes disagree on the state (both ON or
|
||||
# both OFF).
|
||||
if self._negative_node.status._val == self._node.status._val:
|
||||
# The states disagree, therefore we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
|
||||
def _negative_node_control_handler(self, event: object) -> None:
|
||||
"""Handle an "On" control event from the "negative" node."""
|
||||
@@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore primary node status updates.
|
||||
"""Primary node status updates.
|
||||
|
||||
We listen directly to the Control events on all nodes for this
|
||||
device.
|
||||
We MOSTLY ignore these updates, as we listen directly to the Control
|
||||
events on all nodes for this device. However, there is one edge case:
|
||||
If a leak sensor is unknown, due to a recent reboot of the ISY, the
|
||||
status will get updated to dry upon the first heartbeat. This status
|
||||
update is the only way that a leak sensor's status changes without
|
||||
an accompanying Control event, so we need to watch for it.
|
||||
"""
|
||||
pass
|
||||
if self._status_was_unknown and self._computed_state is None:
|
||||
self._computed_state = bool(int(self._node.status))
|
||||
self._status_was_unknown = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
|
||||
82
homeassistant/components/binary_sensor/konnected.py
Normal file
82
homeassistant/components/binary_sensor/konnected.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Support for wired binary sensors attached to a Konnected device.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.konnected/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.konnected import (
|
||||
DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_ENTITY_ID,
|
||||
ATTR_STATE)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['konnected']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up binary sensors attached to a Konnected device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[KONNECTED_DOMAIN]
|
||||
device_id = discovery_info['device_id']
|
||||
sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data)
|
||||
for pin_num, pin_data in
|
||||
data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()]
|
||||
async_add_devices(sensors)
|
||||
|
||||
|
||||
class KonnectedBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Konnected binary sensor."""
|
||||
|
||||
def __init__(self, device_id, pin_num, data):
|
||||
"""Initialize the binary sensor."""
|
||||
self._data = data
|
||||
self._device_id = device_id
|
||||
self._pin_num = pin_num
|
||||
self._state = self._data.get(ATTR_STATE)
|
||||
self._device_class = self._data.get(CONF_TYPE)
|
||||
self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format(
|
||||
device_id, PIN_TO_ZONE[pin_num]))
|
||||
_LOGGER.debug('Created new Konnected sensor: %s', self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Store entity_id and register state change callback."""
|
||||
self._data[ATTR_ENTITY_ID] = self.entity_id
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_SENSOR_UPDATE.format(self.entity_id),
|
||||
self.async_set_state)
|
||||
|
||||
@callback
|
||||
def async_set_state(self, state):
|
||||
"""Update the sensor's state."""
|
||||
self._state = state
|
||||
self.async_schedule_update_ha_state()
|
||||
@@ -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
|
||||
|
||||
@@ -31,7 +31,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
sensors = []
|
||||
hub = hass.data[MYCHEVY_DOMAIN]
|
||||
for sconfig in SENSORS:
|
||||
sensors.append(EVBinarySensor(hub, sconfig))
|
||||
for car in hub.cars:
|
||||
sensors.append(EVBinarySensor(hub, sconfig, car.vid))
|
||||
|
||||
async_add_devices(sensors)
|
||||
|
||||
@@ -45,16 +46,18 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, connection, config):
|
||||
def __init__(self, connection, config, car_vid):
|
||||
"""Initialize sensor with car connection."""
|
||||
self._conn = connection
|
||||
self._name = config.name
|
||||
self._attr = config.attr
|
||||
self._type = config.device_class
|
||||
self._is_on = None
|
||||
|
||||
self._car_vid = car_vid
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(
|
||||
'{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name)))
|
||||
'{}_{}_{}'.format(MYCHEVY_DOMAIN,
|
||||
slugify(self._car.name),
|
||||
slugify(self._name)))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -66,6 +69,11 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
"""Return if on."""
|
||||
return self._is_on
|
||||
|
||||
@property
|
||||
def _car(self):
|
||||
"""Return the car."""
|
||||
return self._conn.get_car(self._car_vid)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@@ -75,8 +83,8 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
@callback
|
||||
def async_update_callback(self):
|
||||
"""Update state."""
|
||||
if self._conn.car is not None:
|
||||
self._is_on = getattr(self._conn.car, self._attr, None)
|
||||
if self._car is not None:
|
||||
self._is_on = getattr(self._car, self._attr, None)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
||||
@@ -13,7 +13,6 @@ import voluptuous as vol
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.netatmo import CameraData
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import CONF_TIMEOUT
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -61,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the access to Netatmo binary sensor."""
|
||||
netatmo = get_component('netatmo')
|
||||
netatmo = hass.components.netatmo
|
||||
home = config.get(CONF_HOME)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
if timeout is None:
|
||||
|
||||
70
homeassistant/components/binary_sensor/qwikswitch.py
Normal file
70
homeassistant/components/binary_sensor/qwikswitch.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Support for Qwikswitch Binary Sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.qwikswitch/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.qwikswitch import QSEntity, DOMAIN as QWIKSWITCH
|
||||
from homeassistant.core import callback
|
||||
|
||||
DEPENDENCIES = [QWIKSWITCH]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, _, add_devices, discovery_info=None):
|
||||
"""Add binary sensor from the main Qwikswitch component."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
qsusb = hass.data[QWIKSWITCH]
|
||||
_LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s",
|
||||
qsusb, discovery_info)
|
||||
devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]]
|
||||
add_devices(devs)
|
||||
|
||||
|
||||
class QSBinarySensor(QSEntity, BinarySensorDevice):
|
||||
"""Sensor based on a Qwikswitch relay/dimmer module."""
|
||||
|
||||
_val = False
|
||||
|
||||
def __init__(self, sensor):
|
||||
"""Initialize the sensor."""
|
||||
from pyqwikswitch import SENSORS
|
||||
|
||||
super().__init__(sensor['id'], sensor['name'])
|
||||
self.channel = sensor['channel']
|
||||
sensor_type = sensor['type']
|
||||
|
||||
self._decode, _ = SENSORS[sensor_type]
|
||||
self._invert = not sensor.get('invert', False)
|
||||
self._class = sensor.get('class', 'door')
|
||||
|
||||
@callback
|
||||
def update_packet(self, packet):
|
||||
"""Receive update packet from QSUSB."""
|
||||
val = self._decode(packet, channel=self.channel)
|
||||
_LOGGER.debug("Update %s (%s:%s) decoded as %s: %s",
|
||||
self.entity_id, self.qsid, self.channel, val, packet)
|
||||
if val is not None:
|
||||
self._val = bool(val)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Check if device is on (non-zero)."""
|
||||
return self._val == self._invert
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique identifier for this sensor."""
|
||||
return "qs{}:{}".format(self.qsid, self.channel)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._class
|
||||
@@ -10,7 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import rfxtrx
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice)
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES,
|
||||
CONF_FIRE_EVENT, CONF_OFF_DELAY)
|
||||
@@ -29,8 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICES, default={}): {
|
||||
cv.string: vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS):
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_OFF_DELAY):
|
||||
vol.Any(cv.time_period, cv.positive_timedelta),
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['tapsaff==0.1.3']
|
||||
REQUIREMENTS = ['tapsaff==0.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -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.2']
|
||||
REQUIREMENTS = ['numpy==1.14.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ https://home-assistant.io/components/binary_sensor.wemo/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['wemo']
|
||||
|
||||
@@ -25,18 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
device = discovery.device_from_description(location, mac)
|
||||
|
||||
if device:
|
||||
add_devices_callback([WemoBinarySensor(device)])
|
||||
add_devices_callback([WemoBinarySensor(hass, device)])
|
||||
|
||||
|
||||
class WemoBinarySensor(BinarySensorDevice):
|
||||
"""Representation a WeMo binary sensor."""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize the WeMo sensor."""
|
||||
self.wemo = device
|
||||
self._state = None
|
||||
|
||||
wemo = get_component('wemo')
|
||||
wemo = hass.components.wemo
|
||||
wemo.SUBSCRIPTION_REGISTRY.register(self.wemo)
|
||||
wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback)
|
||||
|
||||
|
||||
@@ -17,16 +17,17 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.9.4']
|
||||
REQUIREMENTS = ['holidays==0.9.5']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
|
||||
'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK',
|
||||
'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland',
|
||||
'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland',
|
||||
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
|
||||
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
|
||||
ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
|
||||
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
|
||||
'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
|
||||
'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
|
||||
'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy',
|
||||
'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL',
|
||||
'NewZealand', 'NZ', 'Northern Ireland',
|
||||
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
||||
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
|
||||
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
|
||||
|
||||
@@ -25,30 +25,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices['binary_sensor']:
|
||||
model = device['model']
|
||||
if model in ['motion', 'sensor_motion.aq2']:
|
||||
if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']:
|
||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||
elif model in ['magnet', 'sensor_magnet.aq2']:
|
||||
elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']:
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'sensor_wleak.aq1':
|
||||
devices.append(XiaomiWaterLeakSensor(device, gateway))
|
||||
elif model == 'smoke':
|
||||
elif model in ['smoke', 'sensor_smoke']:
|
||||
devices.append(XiaomiSmokeSensor(device, gateway))
|
||||
elif model == 'natgas':
|
||||
elif model in ['natgas', 'sensor_natgas']:
|
||||
devices.append(XiaomiNatgasSensor(device, gateway))
|
||||
elif model in ['switch', 'sensor_switch.aq2', 'sensor_switch.aq3']:
|
||||
devices.append(XiaomiButton(device, 'Switch', 'status',
|
||||
elif model in ['switch', 'sensor_switch',
|
||||
'sensor_switch.aq2', 'sensor_switch.aq3']:
|
||||
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||
data_key = 'status'
|
||||
else:
|
||||
data_key = 'channel_0'
|
||||
devices.append(XiaomiButton(device, 'Switch', data_key,
|
||||
hass, gateway))
|
||||
elif model == '86sw1':
|
||||
elif model in ['86sw1', 'sensor_86sw1.aq1']:
|
||||
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
|
||||
hass, gateway))
|
||||
elif model == '86sw2':
|
||||
elif model in ['86sw2', 'sensor_86sw2.aq1']:
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
|
||||
'channel_0', hass, gateway))
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
|
||||
'channel_1', hass, gateway))
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Both)',
|
||||
'dual_channel', hass, gateway))
|
||||
elif model == 'cube':
|
||||
elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']:
|
||||
devices.append(XiaomiCube(device, hass, gateway))
|
||||
add_devices(devices)
|
||||
|
||||
@@ -129,8 +134,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
"""Initialize the XiaomiMotionSensor."""
|
||||
self._hass = hass
|
||||
self._no_motion_since = 0
|
||||
if 'proto' not in device or int(device['proto'][0:1]) == 1:
|
||||
data_key = 'status'
|
||||
else:
|
||||
data_key = 'motion_status'
|
||||
XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub,
|
||||
'status', 'motion')
|
||||
data_key, 'motion')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
||||
@@ -31,12 +31,21 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
from zigpy.zcl.clusters.general import OnOff
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
if IasZone.cluster_id in discovery_info['in_clusters']:
|
||||
await _async_setup_iaszone(hass, config, async_add_devices,
|
||||
discovery_info)
|
||||
elif OnOff.cluster_id in discovery_info['out_clusters']:
|
||||
await _async_setup_remote(hass, config, async_add_devices,
|
||||
discovery_info)
|
||||
|
||||
in_clusters = discovery_info['in_clusters']
|
||||
|
||||
async def _async_setup_iaszone(hass, config, async_add_devices,
|
||||
discovery_info):
|
||||
device_class = None
|
||||
cluster = in_clusters[IasZone.cluster_id]
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
|
||||
if discovery_info['new_join']:
|
||||
await cluster.bind()
|
||||
ieee = cluster.endpoint.device.application.ieee
|
||||
@@ -53,8 +62,34 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
async_add_devices([sensor], update_before_add=True)
|
||||
|
||||
|
||||
async def _async_setup_remote(hass, config, async_add_devices, discovery_info):
|
||||
|
||||
async def safe(coro):
|
||||
"""Run coro, catching ZigBee delivery errors, and ignoring them."""
|
||||
import zigpy.exceptions
|
||||
try:
|
||||
await coro
|
||||
except zigpy.exceptions.DeliveryError as exc:
|
||||
_LOGGER.warning("Ignoring error during setup: %s", exc)
|
||||
|
||||
if discovery_info['new_join']:
|
||||
from zigpy.zcl.clusters.general import OnOff, LevelControl
|
||||
out_clusters = discovery_info['out_clusters']
|
||||
if OnOff.cluster_id in out_clusters:
|
||||
cluster = out_clusters[OnOff.cluster_id]
|
||||
await safe(cluster.bind())
|
||||
await safe(cluster.configure_reporting(0, 0, 600, 1))
|
||||
if LevelControl.cluster_id in out_clusters:
|
||||
cluster = out_clusters[LevelControl.cluster_id]
|
||||
await safe(cluster.bind())
|
||||
await safe(cluster.configure_reporting(0, 1, 600, 1))
|
||||
|
||||
sensor = Switch(**discovery_info)
|
||||
async_add_devices([sensor], update_before_add=True)
|
||||
|
||||
|
||||
class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
"""THe ZHA Binary Sensor."""
|
||||
"""The ZHA Binary Sensor."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
||||
@@ -73,7 +108,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
if self._state == 'unknown':
|
||||
if self._state is None:
|
||||
return False
|
||||
return bool(self._state)
|
||||
|
||||
@@ -98,7 +133,121 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
from bellows.types.basic import uint16_t
|
||||
|
||||
result = await zha.safe_read(self._endpoint.ias_zone,
|
||||
['zone_status'])
|
||||
['zone_status'],
|
||||
allow_cache=False)
|
||||
state = result.get('zone_status', self._state)
|
||||
if isinstance(state, (int, uint16_t)):
|
||||
self._state = result.get('zone_status', self._state) & 3
|
||||
|
||||
|
||||
class Switch(zha.Entity, BinarySensorDevice):
|
||||
"""ZHA switch/remote controller/button."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
||||
class OnOffListener:
|
||||
"""Listener for the OnOff ZigBee cluster."""
|
||||
|
||||
def __init__(self, entity):
|
||||
"""Initialize OnOffListener."""
|
||||
self._entity = entity
|
||||
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
"""Handle commands received to this cluster."""
|
||||
if command_id in (0x0000, 0x0040):
|
||||
self._entity.set_state(False)
|
||||
elif command_id in (0x0001, 0x0041, 0x0042):
|
||||
self._entity.set_state(True)
|
||||
elif command_id == 0x0002:
|
||||
self._entity.set_state(not self._entity.is_on)
|
||||
|
||||
def attribute_updated(self, attrid, value):
|
||||
"""Handle attribute updates on this cluster."""
|
||||
if attrid == 0:
|
||||
self._entity.set_state(value)
|
||||
|
||||
def zdo_command(self, *args, **kwargs):
|
||||
"""Handle ZDO commands on this cluster."""
|
||||
pass
|
||||
|
||||
class LevelListener:
|
||||
"""Listener for the LevelControl ZigBee cluster."""
|
||||
|
||||
def __init__(self, entity):
|
||||
"""Initialize LevelListener."""
|
||||
self._entity = entity
|
||||
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
"""Handle commands received to this cluster."""
|
||||
if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off
|
||||
self._entity.set_level(args[0])
|
||||
elif command_id in (0x0001, 0x0005): # move, -with_on_off
|
||||
# We should dim slowly -- for now, just step once
|
||||
rate = args[1]
|
||||
if args[0] == 0xff:
|
||||
rate = 10 # Should read default move rate
|
||||
self._entity.move_level(-rate if args[0] else rate)
|
||||
elif command_id == 0x0002: # step
|
||||
# Step (technically shouldn't change on/off)
|
||||
self._entity.move_level(-args[1] if args[0] else args[1])
|
||||
|
||||
def attribute_update(self, attrid, value):
|
||||
"""Handle attribute updates on this cluster."""
|
||||
if attrid == 0:
|
||||
self._entity.set_level(value)
|
||||
|
||||
def zdo_command(self, *args, **kwargs):
|
||||
"""Handle ZDO commands on this cluster."""
|
||||
pass
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize Switch."""
|
||||
super().__init__(**kwargs)
|
||||
self._state = True
|
||||
self._level = 255
|
||||
from zigpy.zcl.clusters import general
|
||||
self._out_listeners = {
|
||||
general.OnOff.cluster_id: self.OnOffListener(self),
|
||||
general.LevelControl.cluster_id: self.LevelListener(self),
|
||||
}
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
self._device_state_attributes.update({
|
||||
'level': self._state and self._level or 0
|
||||
})
|
||||
return self._device_state_attributes
|
||||
|
||||
def move_level(self, change):
|
||||
"""Increment the level, setting state if appropriate."""
|
||||
if not self._state and change > 0:
|
||||
self._level = 0
|
||||
self._level = min(255, max(0, self._level + change))
|
||||
self._state = bool(self._level)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_level(self, level):
|
||||
"""Set the level, setting state if appropriate."""
|
||||
self._level = level
|
||||
self._state = bool(self._level)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_state(self, state):
|
||||
"""Set the state."""
|
||||
self._state = state
|
||||
if self._level == 0:
|
||||
self._level = 255
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
from zigpy.zcl.clusters.general import OnOff
|
||||
result = await zha.safe_read(
|
||||
self._endpoint.out_clusters[OnOff.cluster_id], ['on_off'])
|
||||
self._state = result.get('on_off', self._state)
|
||||
|
||||
@@ -14,13 +14,13 @@ 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']
|
||||
REQUIREMENTS = ['bimmer_connected==0.5.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'bmw_connected_drive'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
ATTR_VIN = 'vin'
|
||||
|
||||
ACCOUNT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
@@ -35,35 +35,40 @@ CONFIG_SCHEMA = vol.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'
|
||||
|
||||
def setup(hass, config):
|
||||
_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():
|
||||
username = account_config[CONF_USERNAME]
|
||||
password = account_config[CONF_PASSWORD]
|
||||
region = account_config[CONF_REGION]
|
||||
_LOGGER.debug('Adding new account %s', name)
|
||||
bimmer = BMWConnectedDriveAccount(username, password, region, 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)
|
||||
accounts.append(setup_account(account_config, hass, name))
|
||||
|
||||
hass.data[DOMAIN] = accounts
|
||||
|
||||
for account in accounts:
|
||||
account.update()
|
||||
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)
|
||||
@@ -71,6 +76,48 @@ def setup(hass, 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."""
|
||||
|
||||
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
|
||||
auxiliary 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.
|
||||
@@ -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,18 +46,22 @@ 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)
|
||||
@@ -80,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:
|
||||
|
||||
@@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import asyncio
|
||||
import base64
|
||||
import collections
|
||||
from contextlib import suppress
|
||||
from datetime import timedelta
|
||||
@@ -13,20 +14,20 @@ import logging
|
||||
import hashlib
|
||||
from random import SystemRandom
|
||||
|
||||
import aiohttp
|
||||
import attr
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
from homeassistant.components import websocket_api
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'camera'
|
||||
@@ -53,6 +54,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
|
||||
_RND = SystemRandom()
|
||||
|
||||
FALLBACK_STREAM_INTERVAL = 1 # seconds
|
||||
MIN_STREAM_INTERVAL = 0.5 # seconds
|
||||
|
||||
CAMERA_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
@@ -61,6 +65,20 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
|
||||
vol.Required(ATTR_FILENAME): cv.template
|
||||
})
|
||||
|
||||
WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail'
|
||||
SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
'type': WS_TYPE_CAMERA_THUMBNAIL,
|
||||
'entity_id': cv.entity_id
|
||||
})
|
||||
|
||||
|
||||
@attr.s
|
||||
class Image:
|
||||
"""Represent an image."""
|
||||
|
||||
content_type = attr.ib(type=str)
|
||||
content = attr.ib(type=bytes)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def enable_motion_detection(hass, entity_id=None):
|
||||
@@ -89,43 +107,40 @@ def async_snapshot(hass, filename, entity_id=None):
|
||||
|
||||
|
||||
@bind_hass
|
||||
@asyncio.coroutine
|
||||
def async_get_image(hass, entity_id, timeout=10):
|
||||
async def async_get_image(hass, entity_id, timeout=10):
|
||||
"""Fetch an image from a camera entity."""
|
||||
websession = async_get_clientsession(hass)
|
||||
state = hass.states.get(entity_id)
|
||||
component = hass.data.get(DOMAIN)
|
||||
|
||||
if state is None:
|
||||
raise HomeAssistantError(
|
||||
"No entity '{0}' for grab an image".format(entity_id))
|
||||
if component is None:
|
||||
raise HomeAssistantError('Camera component not setup')
|
||||
|
||||
url = "{0}{1}".format(
|
||||
hass.config.api.base_url,
|
||||
state.attributes.get(ATTR_ENTITY_PICTURE)
|
||||
)
|
||||
camera = component.get_entity(entity_id)
|
||||
|
||||
try:
|
||||
if camera is None:
|
||||
raise HomeAssistantError('Camera not found')
|
||||
|
||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
response = yield from websession.get(url)
|
||||
image = await camera.async_camera_image()
|
||||
|
||||
if response.status != 200:
|
||||
raise HomeAssistantError("Error {0} on {1}".format(
|
||||
response.status, url))
|
||||
if image:
|
||||
return Image(camera.content_type, image)
|
||||
|
||||
image = yield from response.read()
|
||||
return image
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
raise HomeAssistantError("Can't connect to {0}".format(url))
|
||||
raise HomeAssistantError('Unable to get image')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the camera component."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
component = hass.data[DOMAIN] = \
|
||||
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.http.register_view(CameraImageView(component))
|
||||
hass.http.register_view(CameraMjpegStream(component))
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail,
|
||||
SCHEMA_WS_CAMERA_THUMBNAIL
|
||||
)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
|
||||
@@ -241,6 +256,11 @@ class Camera(Entity):
|
||||
"""Return the camera model."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def frame_interval(self):
|
||||
"""Return the interval between frames of the mjpeg stream."""
|
||||
return 0.5
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
@@ -252,19 +272,17 @@ class Camera(Entity):
|
||||
"""
|
||||
return self.hass.async_add_job(self.camera_image)
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_still_stream(self, request, interval):
|
||||
"""Generate an HTTP MJPEG stream from camera images.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
response = web.StreamResponse()
|
||||
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--frameboundary')
|
||||
yield from response.prepare(request)
|
||||
await response.prepare(request)
|
||||
|
||||
async def write(img_bytes):
|
||||
async def write_to_mjpeg_stream(img_bytes):
|
||||
"""Write image to stream."""
|
||||
await response.write(bytes(
|
||||
'--frameboundary\r\n'
|
||||
@@ -277,21 +295,21 @@ class Camera(Entity):
|
||||
|
||||
try:
|
||||
while True:
|
||||
img_bytes = yield from self.async_camera_image()
|
||||
img_bytes = await self.async_camera_image()
|
||||
if not img_bytes:
|
||||
break
|
||||
|
||||
if img_bytes and img_bytes != last_image:
|
||||
yield from write(img_bytes)
|
||||
await write_to_mjpeg_stream(img_bytes)
|
||||
|
||||
# Chrome seems to always ignore first picture,
|
||||
# print it twice.
|
||||
if last_image is None:
|
||||
yield from write(img_bytes)
|
||||
await write_to_mjpeg_stream(img_bytes)
|
||||
|
||||
last_image = img_bytes
|
||||
|
||||
yield from asyncio.sleep(.5)
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
@@ -299,7 +317,16 @@ class Camera(Entity):
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
await response.write_eof()
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Serve an HTTP MJPEG stream from the camera.
|
||||
|
||||
This method can be overridden by camera plaforms to proxy
|
||||
a direct stream from the camera.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.handle_async_still_stream(request, self.frame_interval)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -329,20 +356,20 @@ class Camera(Entity):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the camera state attributes."""
|
||||
attr = {
|
||||
attrs = {
|
||||
'access_token': self.access_tokens[-1],
|
||||
}
|
||||
|
||||
if self.model:
|
||||
attr['model_name'] = self.model
|
||||
attrs['model_name'] = self.model
|
||||
|
||||
if self.brand:
|
||||
attr['brand'] = self.brand
|
||||
attrs['brand'] = self.brand
|
||||
|
||||
if self.motion_detection_enabled:
|
||||
attr['motion_detection'] = self.motion_detection_enabled
|
||||
attrs['motion_detection'] = self.motion_detection_enabled
|
||||
|
||||
return attr
|
||||
return attrs
|
||||
|
||||
@callback
|
||||
def async_update_token(self):
|
||||
@@ -411,7 +438,43 @@ class CameraMjpegStream(CameraView):
|
||||
url = '/api/camera_proxy_stream/{entity_id}'
|
||||
name = 'api:camera:stream'
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle(self, request, camera):
|
||||
"""Serve camera image."""
|
||||
yield from camera.handle_async_mjpeg_stream(request)
|
||||
async def handle(self, request, camera):
|
||||
"""Serve camera stream, possibly with interval."""
|
||||
interval = request.query.get('interval')
|
||||
if interval is None:
|
||||
await camera.handle_async_mjpeg_stream(request)
|
||||
return
|
||||
|
||||
try:
|
||||
# Compose camera stream from stills
|
||||
interval = float(request.query.get('interval'))
|
||||
if interval < MIN_STREAM_INTERVAL:
|
||||
raise ValueError("Stream interval must be be > {}"
|
||||
.format(MIN_STREAM_INTERVAL))
|
||||
await camera.handle_async_still_stream(request, interval)
|
||||
return
|
||||
except ValueError:
|
||||
return web.Response(status=400)
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_camera_thumbnail(hass, connection, msg):
|
||||
"""Handle get camera thumbnail websocket command.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
async def send_camera_still():
|
||||
"""Send a camera still."""
|
||||
try:
|
||||
image = await async_get_image(hass, msg['entity_id'])
|
||||
connection.send_message_outside(websocket_api.result_message(
|
||||
msg['id'], {
|
||||
'content_type': image.content_type,
|
||||
'content': base64.b64encode(image.content).decode('utf-8')
|
||||
}
|
||||
))
|
||||
except HomeAssistantError:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
|
||||
|
||||
hass.async_add_job(send_camera_still())
|
||||
|
||||
@@ -9,7 +9,6 @@ import logging
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['bloomsky']
|
||||
|
||||
@@ -17,7 +16,7 @@ DEPENDENCIES = ['bloomsky']
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up access to BloomSky cameras."""
|
||||
bloomsky = get_component('bloomsky')
|
||||
bloomsky = hass.components.bloomsky
|
||||
for device in bloomsky.BLOOMSKY.devices.values():
|
||||
add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
|
||||
|
||||
|
||||
58
homeassistant/components/camera/familyhub.py
Normal file
58
homeassistant/components/camera/familyhub.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Family Hub camera for Samsung Refrigerators.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/camera.familyhub/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-family-hub-local==0.0.2']
|
||||
|
||||
DEFAULT_NAME = 'FamilyHub Camera'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_IP_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Family Hub Camera."""
|
||||
from pyfamilyhublocal import FamilyHubCam
|
||||
address = config.get(CONF_IP_ADDRESS)
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
family_hub_cam = FamilyHubCam(address, hass.loop, session)
|
||||
|
||||
async_add_devices([FamilyHubCamera(name, family_hub_cam)], True)
|
||||
|
||||
|
||||
class FamilyHubCamera(Camera):
|
||||
"""The representation of a Family Hub camera."""
|
||||
|
||||
def __init__(self, name, family_hub_cam):
|
||||
"""Initialize camera component."""
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self.family_hub_cam = family_hub_cam
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response."""
|
||||
return await self.family_hub_cam.async_get_cam_image()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
@@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_CONTENT_TYPE = 'content_type'
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
|
||||
CONF_STILL_IMAGE_URL = 'still_image_url'
|
||||
CONF_FRAMERATE = 'framerate'
|
||||
|
||||
DEFAULT_NAME = 'Generic Camera'
|
||||
|
||||
@@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
|
||||
vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
@@ -62,6 +64,7 @@ class GenericCamera(Camera):
|
||||
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
||||
self._still_image_url.hass = hass
|
||||
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
|
||||
self._frame_interval = 1 / device_info[CONF_FRAMERATE]
|
||||
self.content_type = device_info[CONF_CONTENT_TYPE]
|
||||
|
||||
username = device_info.get(CONF_USERNAME)
|
||||
@@ -78,6 +81,11 @@ class GenericCamera(Camera):
|
||||
self._last_url = None
|
||||
self._last_image = None
|
||||
|
||||
@property
|
||||
def frame_interval(self):
|
||||
"""Return the interval between frames of the mjpeg stream."""
|
||||
return self._frame_interval
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
|
||||
@@ -11,31 +11,44 @@ import os
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.camera import (
|
||||
Camera, CAMERA_SERVICE_SCHEMA, DOMAIN, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FILE_PATH = 'file_path'
|
||||
|
||||
DEFAULT_NAME = 'Local File'
|
||||
SERVICE_UPDATE_FILE_PATH = 'local_file_update_file_path'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_FILE_PATH): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend({
|
||||
vol.Required(CONF_FILE_PATH): cv.string
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Camera that works with local files."""
|
||||
file_path = config[CONF_FILE_PATH]
|
||||
camera = LocalFile(config[CONF_NAME], file_path)
|
||||
|
||||
# check filepath given is readable
|
||||
if not os.access(file_path, os.R_OK):
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
config[CONF_NAME], file_path)
|
||||
def update_file_path_service(call):
|
||||
"""Update the file path."""
|
||||
file_path = call.data.get(CONF_FILE_PATH)
|
||||
camera.update_file_path(file_path)
|
||||
return True
|
||||
|
||||
add_devices([LocalFile(config[CONF_NAME], file_path)])
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE_FILE_PATH,
|
||||
update_file_path_service,
|
||||
schema=CAMERA_SERVICE_UPDATE_FILE_PATH)
|
||||
|
||||
add_devices([camera])
|
||||
|
||||
|
||||
class LocalFile(Camera):
|
||||
@@ -46,6 +59,7 @@ class LocalFile(Camera):
|
||||
super().__init__()
|
||||
|
||||
self._name = name
|
||||
self.check_file_path_access(file_path)
|
||||
self._file_path = file_path
|
||||
# Set content type of local file
|
||||
content, _ = mimetypes.guess_type(file_path)
|
||||
@@ -61,7 +75,26 @@ class LocalFile(Camera):
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
self._name, self._file_path)
|
||||
|
||||
def check_file_path_access(self, file_path):
|
||||
"""Check that filepath given is readable."""
|
||||
if not os.access(file_path, os.R_OK):
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
self._name, file_path)
|
||||
|
||||
def update_file_path(self, file_path):
|
||||
"""Update the file_path."""
|
||||
self.check_file_path_access(file_path)
|
||||
self._file_path = file_path
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the camera state attributes."""
|
||||
return {
|
||||
'file_path': self._file_path,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_VERIFY_SSL
|
||||
from homeassistant.components.netatmo import CameraData
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['netatmo']
|
||||
@@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up access to Netatmo cameras."""
|
||||
netatmo = get_component('netatmo')
|
||||
netatmo = hass.components.netatmo
|
||||
home = config.get(CONF_HOME)
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL, True)
|
||||
import lnetatmo
|
||||
|
||||
@@ -24,6 +24,16 @@ snapshot:
|
||||
description: Template of a Filename. Variable is entity_id.
|
||||
example: '/tmp/snapshot_{{ entity_id }}'
|
||||
|
||||
local_file_update_file_path:
|
||||
description: Update the file_path for a local_file camera.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to update.
|
||||
example: 'camera.local_file'
|
||||
file_path:
|
||||
description: Path to the new image file.
|
||||
example: '/images/newimage.jpg'
|
||||
|
||||
onvif_ptz:
|
||||
description: Pan/Tilt/Zoom service for ONVIF camera.
|
||||
fields:
|
||||
@@ -39,4 +49,3 @@ onvif_ptz:
|
||||
zoom:
|
||||
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
|
||||
example: "ZOOM_IN"
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
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()
|
||||
@@ -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])
|
||||
@@ -115,7 +115,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
"""List of available fan modes."""
|
||||
return ['Auto', 'Min', 'Normal', 'Max']
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -143,9 +143,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
self._values[value_type] = value
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target temperature."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
@@ -153,9 +153,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
self._values[set_req.V_HVAC_SPEED] = fan_mode
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set new target temperature."""
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, self.value_type,
|
||||
@@ -163,7 +163,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
self._values[self.value_type] = operation_mode
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,6 @@ from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['netatmo']
|
||||
@@ -42,7 +41,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the NetAtmo Thermostat."""
|
||||
netatmo = get_component('netatmo')
|
||||
netatmo = hass.components.netatmo
|
||||
device = config.get(CONF_RELAY)
|
||||
|
||||
import lnetatmo
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
REQUIREMENTS = ['pysensibo==1.0.2']
|
||||
REQUIREMENTS = ['pysensibo==1.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -11,9 +11,11 @@ import voluptuous as vol
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice)
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW,
|
||||
SUPPORT_HOLD_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||
ClimateDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT,
|
||||
CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS,
|
||||
@@ -27,14 +29,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_FAN_STATE = 'fan_state'
|
||||
ATTR_HVAC_STATE = 'hvac_state'
|
||||
|
||||
CONF_HUMIDIFIER = 'humidifier'
|
||||
|
||||
DEFAULT_SSL = False
|
||||
|
||||
VALID_FAN_STATES = [STATE_ON, STATE_AUTO]
|
||||
VALID_THERMOSTAT_MODES = [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_AUTO]
|
||||
|
||||
HOLD_MODE_OFF = 'off'
|
||||
HOLD_MODE_TEMPERATURE = 'temperature'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_HUMIDIFIER, default=True): cv.boolean,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Optional(CONF_TIMEOUT, default=5):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
@@ -50,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
humidifier = config.get(CONF_HUMIDIFIER)
|
||||
|
||||
if config.get(CONF_SSL):
|
||||
proto = 'https'
|
||||
@@ -60,15 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
addr=host, timeout=timeout, user=username, password=password,
|
||||
proto=proto)
|
||||
|
||||
add_devices([VenstarThermostat(client)], True)
|
||||
add_devices([VenstarThermostat(client, humidifier)], True)
|
||||
|
||||
|
||||
class VenstarThermostat(ClimateDevice):
|
||||
"""Representation of a Venstar thermostat."""
|
||||
|
||||
def __init__(self, client):
|
||||
def __init__(self, client, humidifier):
|
||||
"""Initialize the thermostat."""
|
||||
self._client = client
|
||||
self._humidifier = humidifier
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
@@ -81,14 +91,18 @@ class VenstarThermostat(ClimateDevice):
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE |
|
||||
SUPPORT_HOLD_MODE)
|
||||
|
||||
if self._client.mode == self._client.MODE_AUTO:
|
||||
features |= (SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
|
||||
if self._client.hum_active == 1:
|
||||
features |= SUPPORT_TARGET_HUMIDITY
|
||||
if (self._humidifier and
|
||||
hasattr(self._client, 'hum_active')):
|
||||
features |= (SUPPORT_TARGET_HUMIDITY |
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH |
|
||||
SUPPORT_TARGET_HUMIDITY_LOW)
|
||||
|
||||
return features
|
||||
|
||||
@@ -197,6 +211,18 @@ class VenstarThermostat(ClimateDevice):
|
||||
"""Return the maximum humidity. Hardcoded to 60 in API."""
|
||||
return 60
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return the status of away mode."""
|
||||
return self._client.away == self._client.AWAY_AWAY
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return the status of hold mode."""
|
||||
if self._client.schedule == 0:
|
||||
return HOLD_MODE_TEMPERATURE
|
||||
return HOLD_MODE_OFF
|
||||
|
||||
def _set_operation_mode(self, operation_mode):
|
||||
"""Change the operation mode (internal)."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
@@ -259,3 +285,30 @@ class VenstarThermostat(ClimateDevice):
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Failed to change the target humidity level")
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Set the hold mode."""
|
||||
if hold_mode == HOLD_MODE_TEMPERATURE:
|
||||
success = self._client.set_schedule(0)
|
||||
elif hold_mode == HOLD_MODE_OFF:
|
||||
success = self._client.set_schedule(1)
|
||||
else:
|
||||
_LOGGER.error("Unknown hold mode: %s", hold_mode)
|
||||
success = False
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Failed to change the schedule/hold state")
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Activate away mode."""
|
||||
success = self._client.set_away(self._client.AWAY_AWAY)
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Failed to activate away mode")
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Deactivate away mode."""
|
||||
success = self._client.set_away(self._client.AWAY_HOME)
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Failed to deactivate away mode")
|
||||
|
||||
@@ -190,7 +190,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
@property
|
||||
def cool_on(self):
|
||||
"""Return whether or not the heat is actually heating."""
|
||||
return self.wink.heat_on()
|
||||
return self.wink.cool_on()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||
from homeassistant.components.google_assistant import helpers as ga_h
|
||||
from homeassistant.components.google_assistant import const as ga_c
|
||||
|
||||
from . import http_api, iot
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
@@ -52,7 +53,8 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
|
||||
GOOGLE_ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
|
||||
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(ga_c.CONF_ROOM_HINT): cv.string,
|
||||
})
|
||||
|
||||
ASSISTANT_SCHEMA = vol.Schema({
|
||||
|
||||
@@ -18,37 +18,26 @@ SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
|
||||
ON_DEMAND = ('zwave',)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the config component."""
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
'config', 'config', 'mdi:settings')
|
||||
|
||||
@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."""
|
||||
@@ -58,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
|
||||
|
||||
|
||||
@@ -86,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:
|
||||
@@ -98,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)
|
||||
|
||||
@@ -121,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))
|
||||
@@ -133,10 +129,9 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
'result': 'ok',
|
||||
})
|
||||
|
||||
@asyncio.coroutine
|
||||
def read_config(self, hass):
|
||||
async def read_config(self, hass):
|
||||
"""Read the config."""
|
||||
current = yield from hass.async_add_job(
|
||||
current = await hass.async_add_job(
|
||||
_read, hass.config.path(self.path))
|
||||
if not current:
|
||||
current = self._empty_config()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Provide configuration end points for Automations."""
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
import uuid
|
||||
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.components.config import EditIdBasedConfigView
|
||||
from homeassistant.components.automation import (
|
||||
PLATFORM_SCHEMA, DOMAIN, async_reload)
|
||||
@@ -13,8 +16,43 @@ CONFIG_PATH = 'automations.yaml'
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Set up the Automation config API."""
|
||||
hass.http.register_view(EditIdBasedConfigView(
|
||||
hass.http.register_view(EditAutomationConfigView(
|
||||
DOMAIN, 'config', CONFIG_PATH, cv.string,
|
||||
PLATFORM_SCHEMA, post_write_hook=async_reload
|
||||
))
|
||||
return True
|
||||
|
||||
|
||||
class EditAutomationConfigView(EditIdBasedConfigView):
|
||||
"""Edit automation config."""
|
||||
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
index = None
|
||||
for index, cur_value in enumerate(data):
|
||||
# When people copy paste their automations to the config file,
|
||||
# they sometimes forget to add IDs. Fix it here.
|
||||
if CONF_ID not in cur_value:
|
||||
cur_value[CONF_ID] = uuid.uuid4().hex
|
||||
|
||||
elif cur_value[CONF_ID] == config_key:
|
||||
break
|
||||
else:
|
||||
cur_value = OrderedDict()
|
||||
cur_value[CONF_ID] = config_key
|
||||
index = len(data)
|
||||
data.append(cur_value)
|
||||
|
||||
# Iterate through some keys that we want to have ordered in the output
|
||||
updated_value = OrderedDict()
|
||||
for key in ('id', 'alias', 'trigger', 'condition', 'action'):
|
||||
if key in cur_value:
|
||||
updated_value[key] = cur_value[key]
|
||||
if key in new_value:
|
||||
updated_value[key] = new_value[key]
|
||||
|
||||
# We cover all current fields above, but just in case we start
|
||||
# supporting more fields in the future.
|
||||
updated_value.update(cur_value)
|
||||
updated_value.update(new_value)
|
||||
data[index] = updated_value
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Http views to control the config manager."""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
|
||||
|
||||
REQUIREMENTS = ['voluptuous-serialize==1']
|
||||
@@ -16,15 +15,17 @@ def async_setup(hass):
|
||||
"""Enable the Home Assistant views."""
|
||||
hass.http.register_view(ConfigManagerEntryIndexView)
|
||||
hass.http.register_view(ConfigManagerEntryResourceView)
|
||||
hass.http.register_view(ConfigManagerFlowIndexView)
|
||||
hass.http.register_view(ConfigManagerFlowResourceView)
|
||||
hass.http.register_view(
|
||||
ConfigManagerFlowIndexView(hass.config_entries.flow))
|
||||
hass.http.register_view(
|
||||
ConfigManagerFlowResourceView(hass.config_entries.flow))
|
||||
hass.http.register_view(ConfigManagerAvailableFlowView)
|
||||
return True
|
||||
|
||||
|
||||
def _prepare_json(result):
|
||||
"""Convert result for JSON."""
|
||||
if result['type'] != config_entries.RESULT_TYPE_FORM:
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
return result
|
||||
|
||||
import voluptuous_serialize
|
||||
@@ -78,7 +79,7 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
|
||||
return self.json(result)
|
||||
|
||||
|
||||
class ConfigManagerFlowIndexView(HomeAssistantView):
|
||||
class ConfigManagerFlowIndexView(FlowManagerIndexView):
|
||||
"""View to create config flows."""
|
||||
|
||||
url = '/api/config/config_entries/flow'
|
||||
@@ -94,81 +95,16 @@ class ConfigManagerFlowIndexView(HomeAssistantView):
|
||||
hass = request.app['hass']
|
||||
|
||||
return self.json([
|
||||
flow for flow in hass.config_entries.flow.async_progress()
|
||||
if flow['source'] != config_entries.SOURCE_USER])
|
||||
|
||||
@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."""
|
||||
|
||||
@@ -96,6 +96,7 @@ async def async_setup(hass, config):
|
||||
async def process(service):
|
||||
"""Parse text into commands."""
|
||||
text = service.data[ATTR_TEXT]
|
||||
_LOGGER.debug('Processing: <%s>', text)
|
||||
try:
|
||||
await _process(hass, text)
|
||||
except intent.IntentHandleError as err:
|
||||
10
homeassistant/components/conversation/services.yaml
Normal file
10
homeassistant/components/conversation/services.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# Describes the format for available component services
|
||||
|
||||
process:
|
||||
description: Launch a conversation from a transcribed text.
|
||||
fields:
|
||||
text:
|
||||
description: Transcribed text
|
||||
example: Turn all lights on
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for Gogogate2 Garage Doors.
|
||||
Support for Gogogate2 garage Doors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/cover.gogogate2/
|
||||
@@ -11,11 +11,11 @@ import voluptuous as vol
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN,
|
||||
CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED,
|
||||
CONF_IP_ADDRESS, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pygogogate2==0.0.3']
|
||||
REQUIREMENTS = ['pygogogate2==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,9 +25,9 @@ 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.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
@@ -36,10 +36,11 @@ 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)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
username = config.get(CONF_USERNAME)
|
||||
|
||||
mygogogate2 = pygogogate2(username, password, ip_address)
|
||||
|
||||
try:
|
||||
@@ -50,7 +51,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices(MyGogogate2Device(
|
||||
mygogogate2, door, name) for door in devices)
|
||||
return
|
||||
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
@@ -60,7 +60,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return
|
||||
|
||||
|
||||
class MyGogogate2Device(CoverDevice):
|
||||
@@ -72,7 +71,7 @@ class MyGogogate2Device(CoverDevice):
|
||||
self.device_id = device['door']
|
||||
self._name = name or device['name']
|
||||
self._status = device['status']
|
||||
self.available = None
|
||||
self._available = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -97,24 +96,22 @@ class MyGogogate2Device(CoverDevice):
|
||||
@property
|
||||
def available(self):
|
||||
"""Could the device be accessed during the last update call."""
|
||||
return self.available
|
||||
return self._available
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Issue close command to cover."""
|
||||
self.mygogogate2.close_device(self.device_id)
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Issue open command to cover."""
|
||||
self.mygogogate2.open_device(self.device_id)
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def update(self):
|
||||
"""Update status of cover."""
|
||||
try:
|
||||
self._status = self.mygogogate2.get_status(self.device_id)
|
||||
self.available = True
|
||||
self._available = True
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
self._status = STATE_UNKNOWN
|
||||
self.available = False
|
||||
self._status = None
|
||||
self._available = False
|
||||
|
||||
@@ -42,7 +42,7 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
set_req = self.gateway.const.SetReq
|
||||
return self._values.get(set_req.V_DIMMER)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
@@ -53,9 +53,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
self._values[set_req.V_DIMMER] = 100
|
||||
else:
|
||||
self._values[set_req.V_LIGHT] = STATE_ON
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
@@ -66,9 +66,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
self._values[set_req.V_DIMMER] = 0
|
||||
else:
|
||||
self._values[set_req.V_LIGHT] = STATE_OFF
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
set_req = self.gateway.const.SetReq
|
||||
@@ -77,9 +77,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._values[set_req.V_DIMMER] = position
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the device."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']:
|
||||
@@ -79,5 +79,9 @@ class TahomaCover(TahomaDevice, CoverDevice):
|
||||
if self.tahoma_device.type == \
|
||||
'io:RollerShutterWithLowSpeedManagementIOComponent':
|
||||
self.apply_action('setPosition', 'secured')
|
||||
elif self.tahoma_device.type in \
|
||||
('rts:BlindRTSComponent',
|
||||
'io:ExteriorVenetianBlindIOComponent'):
|
||||
self.apply_action('my')
|
||||
else:
|
||||
self.apply_action('stopIdentify')
|
||||
|
||||
25
homeassistant/components/deconz/.translations/bg.json
Normal file
25
homeassistant/components/deconz/.translations/bg.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ",
|
||||
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "\u0425\u043e\u0441\u0442",
|
||||
"port": "\u041f\u043e\u0440\u0442 (\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: '80')"
|
||||
},
|
||||
"title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437"
|
||||
},
|
||||
"link": {
|
||||
"description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"",
|
||||
"title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/cy.json
Normal file
26
homeassistant/components/deconz/.translations/cy.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Pont eisoes wedi'i ffurfweddu",
|
||||
"no_bridges": "Dim pontydd deCONZ wedi eu darganfod",
|
||||
"one_instance_only": "Elfen dim ond yn cefnogi enghraifft deCONZ"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Methu cael allwedd API"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Gwesteiwr",
|
||||
"port": "Port (gwerth diofyn: '80')"
|
||||
},
|
||||
"title": "Diffiniwch porth dad-adeiladu"
|
||||
},
|
||||
"link": {
|
||||
"description": "Datgloi eich porth deCONZ i gofrestru gyda Cynorthwydd Cartref.\n\n1. Ewch i osodiadau system deCONZ \n2. Bwyso botwm \"Datgloi porth\"",
|
||||
"title": "Cysylltu \u00e2 deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
11
homeassistant/components/deconz/.translations/da.json
Normal file
11
homeassistant/components/deconz/.translations/da.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "V\u00e6rt"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/de.json
Normal file
26
homeassistant/components/deconz/.translations/de.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Bridge ist bereits konfiguriert",
|
||||
"no_bridges": "Keine deCON-Bridges entdeckt",
|
||||
"one_instance_only": "Komponente unterst\u00fctzt nur eine deCONZ-Instanz"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (Standartwert : '80')"
|
||||
},
|
||||
"title": "Definieren Sie den deCONZ-Gateway"
|
||||
},
|
||||
"link": {
|
||||
"description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"",
|
||||
"title": "Mit deCONZ verbinden"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +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"
|
||||
}
|
||||
"abort": {
|
||||
"already_configured": "Bridge is already configured",
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"one_instance_only": "Component only supports one deCONZ instance"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Couldn't get an API key"
|
||||
},
|
||||
"abort": {
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"one_instance_only": "Component only supports one deCONZ instance"
|
||||
}
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (default value: '80')"
|
||||
},
|
||||
"title": "Define deCONZ gateway"
|
||||
},
|
||||
"link": {
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button",
|
||||
"title": "Link with deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/deconz/.translations/hu.json
Normal file
22
homeassistant/components/deconz/.translations/hu.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "H\u00e1zigazda (Host)",
|
||||
"port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/ko.json
Normal file
26
homeassistant/components/deconz/.translations/ko.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
|
||||
"no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
|
||||
"one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4 \ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "\ud638\uc2a4\ud2b8",
|
||||
"port": "\ud3ec\ud2b8 (\uae30\ubcf8\uac12: '80')"
|
||||
},
|
||||
"title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758"
|
||||
},
|
||||
"link": {
|
||||
"description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ",
|
||||
"title": "deCONZ \uc640 \uc5f0\uacb0"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/lb.json
Normal file
26
homeassistant/components/deconz/.translations/lb.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Bridge ass schon konfigur\u00e9iert",
|
||||
"no_bridges": "Keng dECONZ bridges fonnt",
|
||||
"one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng deCONZ Instanz"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (Standard Wert: '80')"
|
||||
},
|
||||
"title": "deCONZ gateway d\u00e9fin\u00e9ieren"
|
||||
},
|
||||
"link": {
|
||||
"description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen",
|
||||
"title": "Link mat deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/nl.json
Normal file
26
homeassistant/components/deconz/.translations/nl.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Bridge is al geconfigureerd",
|
||||
"no_bridges": "Geen deCONZ bruggen ontdekt",
|
||||
"one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n deCONZ instance"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Kon geen API-sleutel ophalen"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Poort (standaard: '80')"
|
||||
},
|
||||
"title": "Definieer deCONZ gateway"
|
||||
},
|
||||
"link": {
|
||||
"description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"",
|
||||
"title": "Koppel met deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/no.json
Normal file
26
homeassistant/components/deconz/.translations/no.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Broen er allerede konfigurert",
|
||||
"no_bridges": "Ingen deCONZ broer oppdaget",
|
||||
"one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"port": "Port (standardverdi: '80')"
|
||||
},
|
||||
"title": "Definer deCONZ-gatewayen"
|
||||
},
|
||||
"link": {
|
||||
"description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen",
|
||||
"title": "Koble til deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/pl.json
Normal file
26
homeassistant/components/deconz/.translations/pl.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Mostek jest ju\u017c skonfigurowany",
|
||||
"no_bridges": "Nie odkryto mostk\u00f3w deCONZ",
|
||||
"one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Nie mo\u017cna uzyska\u0107 klucza API"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (warto\u015b\u0107 domy\u015blna: \"80\")"
|
||||
},
|
||||
"title": "Zdefiniuj bramk\u0119 deCONZ"
|
||||
},
|
||||
"link": {
|
||||
"description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"",
|
||||
"title": "Po\u0142\u0105cz z deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
7
homeassistant/components/deconz/.translations/pt.json
Normal file
7
homeassistant/components/deconz/.translations/pt.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Bridge j\u00e1 est\u00e1 configurada"
|
||||
}
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/ru.json
Normal file
26
homeassistant/components/deconz/.translations/ru.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d",
|
||||
"no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
|
||||
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "\u0425\u043e\u0441\u0442",
|
||||
"port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: '80')"
|
||||
},
|
||||
"title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ"
|
||||
},
|
||||
"link": {
|
||||
"description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb",
|
||||
"title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/sl.json
Normal file
26
homeassistant/components/deconz/.translations/sl.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Most je \u017ee nastavljen",
|
||||
"no_bridges": "Ni odkritih mostov deCONZ",
|
||||
"one_instance_only": "Komponenta podpira le en primerek deCONZ"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Klju\u010da API ni mogo\u010de dobiti"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Gostitelj",
|
||||
"port": "Vrata (privzeta vrednost: '80')"
|
||||
},
|
||||
"title": "Dolo\u010dite deCONZ prehod"
|
||||
},
|
||||
"link": {
|
||||
"description": "Odklenite va\u0161 deCONZ gateway za registracijo z Home Assistant-om. \n1. Pojdite v deCONT sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"",
|
||||
"title": "Povezava z deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
26
homeassistant/components/deconz/.translations/zh-Hans.json
Normal file
26
homeassistant/components/deconz/.translations/zh-Hans.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u6865\u63a5\u5668\u5df2\u914d\u7f6e\u5b8c\u6210",
|
||||
"no_bridges": "\u6ca1\u6709\u53d1\u73b0 deCONZ \u7684\u6865\u63a5\u8bbe\u5907",
|
||||
"one_instance_only": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a deCONZ \u5b9e\u4f8b"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "\u65e0\u6cd5\u83b7\u53d6 API \u5bc6\u94a5"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "\u4e3b\u673a",
|
||||
"port": "\u7aef\u53e3\uff08\u9ed8\u8ba4\u503c\uff1a'80'\uff09"
|
||||
},
|
||||
"title": "\u5b9a\u4e49 deCONZ \u7f51\u5173"
|
||||
},
|
||||
"link": {
|
||||
"description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae",
|
||||
"title": "\u8fde\u63a5 deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
25
homeassistant/components/deconz/.translations/zh-Hant.json
Normal file
25
homeassistant/components/deconz/.translations/zh-Hant.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe",
|
||||
"one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b"
|
||||
},
|
||||
"error": {
|
||||
"no_key": "\u7121\u6cd5\u53d6\u5f97 API key"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "\u4e3b\u6a5f\u7aef",
|
||||
"port": "\u901a\u8a0a\u57e0\uff08\u9810\u8a2d\u503c\uff1a'80'\uff09"
|
||||
},
|
||||
"title": "\u5b9a\u7fa9 deCONZ \u7db2\u95dc"
|
||||
},
|
||||
"link": {
|
||||
"description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215",
|
||||
"title": "\u9023\u7d50\u81f3 deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
}
|
||||
}
|
||||
@@ -4,28 +4,25 @@ Support for deCONZ devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/deconz/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
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, aiohttp_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
CONF_API_KEY, CONF_EVENT, CONF_HOST,
|
||||
CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import EventOrigin, callback
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, async_dispatcher_send)
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
REQUIREMENTS = ['pydeconz==35']
|
||||
# Loading the config flow file will register the flow
|
||||
from .config_flow import configured_hosts
|
||||
from .const import (
|
||||
CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID,
|
||||
DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'deconz'
|
||||
DATA_DECONZ_ID = 'deconz_entities'
|
||||
|
||||
CONFIG_FILE = 'deconz.conf'
|
||||
REQUIREMENTS = ['pydeconz==38']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@@ -35,6 +32,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_DECONZ = 'configure'
|
||||
|
||||
SERVICE_FIELD = 'field'
|
||||
SERVICE_ENTITY = 'entity'
|
||||
SERVICE_DATA = 'data'
|
||||
@@ -46,56 +45,47 @@ 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/)
|
||||
"""
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up services and configuration for deCONZ component."""
|
||||
result = False
|
||||
config_file = await hass.async_add_job(
|
||||
load_json, hass.config.path(CONFIG_FILE))
|
||||
|
||||
async 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)
|
||||
await async_request_configuration(hass, config, deconz_config)
|
||||
|
||||
if config_file:
|
||||
result = await 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 = await async_setup_deconz(hass, config, deconz_config)
|
||||
else:
|
||||
await async_request_configuration(hass, config, deconz_config)
|
||||
return True
|
||||
|
||||
if not result:
|
||||
discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
|
||||
"""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
|
||||
|
||||
|
||||
async def async_setup_deconz(hass, config, deconz_config):
|
||||
"""Set up a deCONZ session.
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up a deCONZ bridge for a config entry.
|
||||
|
||||
Load config, group, light and sensor data for server information.
|
||||
Start websocket for push notification of state changes from deCONZ.
|
||||
"""
|
||||
_LOGGER.debug("deCONZ config %s", deconz_config)
|
||||
from pydeconz import DeconzSession
|
||||
websession = async_get_clientsession(hass)
|
||||
deconz = DeconzSession(hass.loop, websession, **deconz_config)
|
||||
if DOMAIN in hass.data:
|
||||
_LOGGER.error(
|
||||
"Config entry failed since one deCONZ instance already exists")
|
||||
return False
|
||||
|
||||
@callback
|
||||
def async_add_device_callback(device_type, device):
|
||||
"""Called when a new device has been created in deCONZ."""
|
||||
async_dispatcher_send(
|
||||
hass, 'deconz_new_{}'.format(device_type), [device])
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
deconz = DeconzSession(hass.loop, session, **config_entry.data,
|
||||
async_add_device=async_add_device_callback)
|
||||
result = await deconz.async_load_parameters()
|
||||
if result is False:
|
||||
_LOGGER.error("Failed to communicate with deCONZ")
|
||||
@@ -103,10 +93,25 @@ async def async_setup_deconz(hass, config, deconz_config):
|
||||
|
||||
hass.data[DOMAIN] = deconz
|
||||
hass.data[DATA_DECONZ_ID] = {}
|
||||
hass.data[DATA_DECONZ_EVENT] = []
|
||||
hass.data[DATA_DECONZ_UNSUB] = []
|
||||
|
||||
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, component, DOMAIN, {}, config))
|
||||
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, component))
|
||||
|
||||
@callback
|
||||
def async_add_remote(sensors):
|
||||
"""Setup remote from deCONZ."""
|
||||
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
|
||||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_REMOTE:
|
||||
hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor))
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote))
|
||||
|
||||
async_add_remote(deconz.sensors.values())
|
||||
|
||||
deconz.start()
|
||||
|
||||
async def async_configure(call):
|
||||
@@ -137,7 +142,7 @@ async def async_setup_deconz(hass, config, deconz_config):
|
||||
return
|
||||
await deconz.async_put_state(field, data)
|
||||
hass.services.async_register(
|
||||
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA)
|
||||
|
||||
@callback
|
||||
def deconz_shutdown(event):
|
||||
@@ -154,119 +159,41 @@ async def async_setup_deconz(hass, config, deconz_config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_request_configuration(hass, config, deconz_config):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
async def async_configuration_callback(data):
|
||||
"""Set up actions to do when our configuration callback is called."""
|
||||
from pydeconz.utils import async_get_api_key
|
||||
websession = async_get_clientsession(hass)
|
||||
api_key = await async_get_api_key(websession, **deconz_config)
|
||||
if api_key:
|
||||
deconz_config[CONF_API_KEY] = api_key
|
||||
result = await async_setup_deconz(hass, config, deconz_config)
|
||||
if result:
|
||||
await 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",
|
||||
)
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
"""Unload deCONZ config entry."""
|
||||
deconz = hass.data.pop(DOMAIN)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
|
||||
deconz.close()
|
||||
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
|
||||
await hass.config_entries.async_forward_entry_unload(
|
||||
config_entry, component)
|
||||
dispatchers = hass.data[DATA_DECONZ_UNSUB]
|
||||
for unsub_dispatcher in dispatchers:
|
||||
unsub_dispatcher()
|
||||
hass.data[DATA_DECONZ_UNSUB] = []
|
||||
hass.data[DATA_DECONZ_EVENT] = []
|
||||
hass.data[DATA_DECONZ_ID] = []
|
||||
return True
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class DeconzFlowHandler(config_entries.ConfigFlowHandler):
|
||||
"""Handle a deCONZ config flow."""
|
||||
class DeconzEvent(object):
|
||||
"""When you want signals instead of entities.
|
||||
|
||||
VERSION = 1
|
||||
Stateless sensors such as remotes are expected to generate an event
|
||||
instead of a sensor entity in hass.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the deCONZ flow."""
|
||||
self.bridges = []
|
||||
self.deconz_config = {}
|
||||
def __init__(self, hass, device):
|
||||
"""Register callback that will be used for signals."""
|
||||
self._hass = hass
|
||||
self._device = device
|
||||
self._device.register_async_callback(self.async_update_callback)
|
||||
self._event = 'deconz_{}'.format(CONF_EVENT)
|
||||
self._id = slugify(self._device.name)
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
from pydeconz.utils import async_discovery
|
||||
|
||||
if DOMAIN in self.hass.data:
|
||||
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
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
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
|
||||
return self.async_create_entry(
|
||||
title='deCONZ',
|
||||
data=self.deconz_config
|
||||
)
|
||||
else:
|
||||
errors['base'] = 'no_key'
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='link',
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up a 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
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
"""Fire the event if reason is that state is updated."""
|
||||
if reason['state']:
|
||||
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
|
||||
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)
|
||||
|
||||
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
|
||||
)
|
||||
10
homeassistant/components/deconz/const.py
Normal file
10
homeassistant/components/deconz/const.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Constants for the deCONZ component."""
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger('homeassistant.components.deconz')
|
||||
|
||||
DOMAIN = 'deconz'
|
||||
CONFIG_FILE = 'deconz.conf'
|
||||
DATA_DECONZ_EVENT = 'deconz_events'
|
||||
DATA_DECONZ_ID = 'deconz_entities'
|
||||
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
|
||||
@@ -18,6 +18,7 @@
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_utc_time, async_track_state_change)
|
||||
from homeassistant.helpers.sun import is_up, get_astral_event_next
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'device_sun_light_trigger'
|
||||
@@ -48,9 +47,9 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
def async_setup(hass, config):
|
||||
"""Set up the triggers to control lights based on device presence."""
|
||||
logger = logging.getLogger(__name__)
|
||||
device_tracker = get_component('device_tracker')
|
||||
group = get_component('group')
|
||||
light = get_component('light')
|
||||
device_tracker = hass.components.device_tracker
|
||||
group = hass.components.group
|
||||
light = hass.components.light
|
||||
conf = config[DOMAIN]
|
||||
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
|
||||
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS)
|
||||
@@ -58,14 +57,14 @@ def async_setup(hass, config):
|
||||
device_group = conf.get(
|
||||
CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES)
|
||||
device_entity_ids = group.get_entity_ids(
|
||||
hass, device_group, device_tracker.DOMAIN)
|
||||
device_group, device_tracker.DOMAIN)
|
||||
|
||||
if not device_entity_ids:
|
||||
logger.error("No devices found to track")
|
||||
return False
|
||||
|
||||
# Get the light IDs from the specified group
|
||||
light_ids = group.get_entity_ids(hass, light_group, light.DOMAIN)
|
||||
light_ids = group.get_entity_ids(light_group, light.DOMAIN)
|
||||
|
||||
if not light_ids:
|
||||
logger.error("No lights found to turn on")
|
||||
@@ -85,9 +84,9 @@ def async_setup(hass, config):
|
||||
|
||||
def async_turn_on_before_sunset(light_id):
|
||||
"""Turn on lights."""
|
||||
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
|
||||
if not device_tracker.is_on() or light.is_on(light_id):
|
||||
return
|
||||
light.async_turn_on(hass, light_id,
|
||||
light.async_turn_on(light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
|
||||
@@ -129,7 +128,7 @@ def async_setup(hass, config):
|
||||
@callback
|
||||
def check_light_on_dev_state_change(entity, old_state, new_state):
|
||||
"""Handle tracked device state changes."""
|
||||
lights_are_on = group.is_on(hass, light_group)
|
||||
lights_are_on = group.is_on(light_group)
|
||||
light_needed = not (lights_are_on or is_up(hass))
|
||||
|
||||
# These variables are needed for the elif check
|
||||
@@ -139,7 +138,7 @@ def async_setup(hass, config):
|
||||
# Do we need lights?
|
||||
if light_needed:
|
||||
logger.info("Home coming event for %s. Turning lights on", entity)
|
||||
light.async_turn_on(hass, light_ids, profile=light_profile)
|
||||
light.async_turn_on(light_ids, profile=light_profile)
|
||||
|
||||
# Are we in the time span were we would turn on the lights
|
||||
# if someone would be home?
|
||||
@@ -152,7 +151,7 @@ def async_setup(hass, config):
|
||||
# when the fading in started and turn it on if so
|
||||
for index, light_id in enumerate(light_ids):
|
||||
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
||||
light.async_turn_on(hass, light_id)
|
||||
light.async_turn_on(light_id)
|
||||
|
||||
else:
|
||||
# If this light didn't happen to be turned on yet so
|
||||
@@ -169,12 +168,12 @@ def async_setup(hass, config):
|
||||
@callback
|
||||
def turn_off_lights_when_all_leave(entity, old_state, new_state):
|
||||
"""Handle device group state change."""
|
||||
if not group.is_on(hass, light_group):
|
||||
if not group.is_on(light_group):
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"Everyone has left but there are lights on. Turning them off")
|
||||
light.async_turn_off(hass, light_ids)
|
||||
light.async_turn_off(light_ids)
|
||||
|
||||
async_track_state_change(
|
||||
hass, device_group, turn_off_lights_when_all_leave,
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.zone.zone import async_active_zone
|
||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
@@ -23,7 +24,6 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
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
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -33,7 +33,7 @@ from homeassistant.helpers.event import async_track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID,
|
||||
CONF_ICON, ATTR_ICON)
|
||||
CONF_ICON, ATTR_ICON, ATTR_NAME)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -71,7 +71,6 @@ ATTR_GPS = 'gps'
|
||||
ATTR_HOST_NAME = 'host_name'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_SOURCE_TYPE = 'source_type'
|
||||
ATTR_CONSIDER_HOME = 'consider_home'
|
||||
|
||||
@@ -321,7 +320,7 @@ class DeviceTracker(object):
|
||||
# During init, we ignore the group
|
||||
if self.group and self.track_new:
|
||||
self.group.async_set_group(
|
||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||
util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
@@ -356,9 +355,9 @@ class DeviceTracker(object):
|
||||
entity_ids = [dev.entity_id for dev in self.devices.values()
|
||||
if dev.track]
|
||||
|
||||
self.group = get_component('group')
|
||||
self.group = self.hass.components.group
|
||||
self.group.async_set_group(
|
||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||
util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||
name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids)
|
||||
|
||||
@callback
|
||||
@@ -541,7 +540,7 @@ class Device(Entity):
|
||||
elif self.location_name:
|
||||
self._state = self.location_name
|
||||
elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS:
|
||||
zone_state = zone.async_active_zone(
|
||||
zone_state = async_active_zone(
|
||||
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
self._state = STATE_NOT_HOME
|
||||
@@ -605,6 +604,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."""
|
||||
@@ -690,10 +700,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)
|
||||
|
||||
@@ -40,7 +40,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
attributes = {}
|
||||
if rssi is not None:
|
||||
attributes['rssi'] = rssi
|
||||
see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name,
|
||||
see(mac="{}{}".format(BT_PREFIX, mac), host_name=name,
|
||||
attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH)
|
||||
|
||||
def discover_devices():
|
||||
|
||||
@@ -48,8 +48,11 @@ class BMWDeviceTracker(object):
|
||||
return
|
||||
|
||||
_LOGGER.debug('Updating %s', dev_id)
|
||||
|
||||
attrs = {
|
||||
'vin': self.vehicle.vin,
|
||||
}
|
||||
self._see(
|
||||
dev_id=dev_id, host_name=self.vehicle.name,
|
||||
gps=self.vehicle.state.gps_position, icon='mdi:car'
|
||||
gps=self.vehicle.state.gps_position, attributes=attrs,
|
||||
icon='mdi:car'
|
||||
)
|
||||
|
||||
@@ -12,14 +12,18 @@ 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.const import CONF_USERNAME, CONF_PASSWORD, ATTR_ID
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==2.0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==0.4.0']
|
||||
ATTR_ADDRESS = 'address'
|
||||
ATTR_FULL_NAME = 'full_name'
|
||||
ATTR_LAST_SEEN = 'last_seen'
|
||||
ATTR_NICKNAME = 'nickname'
|
||||
|
||||
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
|
||||
|
||||
@@ -60,24 +64,29 @@ class GoogleMapsScanner(object):
|
||||
self.success_init = True
|
||||
|
||||
except InvalidUser:
|
||||
_LOGGER.error('You have specified invalid login credentials')
|
||||
_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))
|
||||
try:
|
||||
dev_id = 'google_maps_{0}'.format(person.id)
|
||||
except TypeError:
|
||||
_LOGGER.warning("No location(s) shared with this account")
|
||||
return
|
||||
|
||||
attrs = {
|
||||
'id': person.id,
|
||||
'nickname': person.nickname,
|
||||
'full_name': person.full_name,
|
||||
'last_seen': person.datetime,
|
||||
'address': person.address
|
||||
ATTR_ADDRESS: person.address,
|
||||
ATTR_FULL_NAME: person.full_name,
|
||||
ATTR_ID: person.id,
|
||||
ATTR_LAST_SEEN: person.datetime,
|
||||
ATTR_NICKNAME: person.nickname,
|
||||
}
|
||||
self.see(
|
||||
dev_id=dev_id,
|
||||
gps=(person.latitude, person.longitude),
|
||||
picture=person.picture_url,
|
||||
source_type=SOURCE_TYPE_GPS,
|
||||
attributes=attrs
|
||||
gps_accuracy=person.accuracy,
|
||||
attributes=attrs,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for the GPSLogger platform.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.gpslogger/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from hmac import compare_digest
|
||||
|
||||
@@ -22,6 +21,7 @@ from homeassistant.components.http import (
|
||||
from homeassistant.components.device_tracker import ( # NOQA
|
||||
DOMAIN, PLATFORM_SCHEMA
|
||||
)
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,8 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType,
|
||||
async_see, discovery_info=None):
|
||||
"""Set up an endpoint for the GPSLogger application."""
|
||||
hass.http.register_view(GPSLoggerView(async_see, config))
|
||||
|
||||
@@ -54,8 +54,7 @@ class GPSLoggerView(HomeAssistantView):
|
||||
# password is set
|
||||
self.requires_auth = self._password is None
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request: Request):
|
||||
async def get(self, request: Request):
|
||||
"""Handle for GPSLogger message received as GET."""
|
||||
hass = request.app['hass']
|
||||
data = request.query
|
||||
|
||||
@@ -14,15 +14,18 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TYPE = "rogers"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -49,6 +52,11 @@ class HitronCODADeviceScanner(DeviceScanner):
|
||||
self._username = config.get(CONF_USERNAME)
|
||||
self._password = config.get(CONF_PASSWORD)
|
||||
|
||||
if config.get(CONF_TYPE) == "shaw":
|
||||
self._type = 'pwd'
|
||||
else:
|
||||
self._type = 'pws'
|
||||
|
||||
self._userid = None
|
||||
|
||||
self.success_init = self._update_info()
|
||||
@@ -74,7 +82,7 @@ class HitronCODADeviceScanner(DeviceScanner):
|
||||
try:
|
||||
data = [
|
||||
('user', self._username),
|
||||
('pws', self._password),
|
||||
(self._type, self._password),
|
||||
]
|
||||
res = requests.post(self._loginurl, data=data, timeout=10)
|
||||
except requests.exceptions.Timeout:
|
||||
|
||||
@@ -13,7 +13,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner)
|
||||
from homeassistant.components.zone import active_zone
|
||||
from homeassistant.components.zone.zone import active_zone
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
@@ -24,8 +24,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyicloud==0.9.1']
|
||||
|
||||
CONF_IGNORED_DEVICES = 'ignored_devices'
|
||||
CONF_ACCOUNTNAME = 'account_name'
|
||||
CONF_MAX_INTERVAL = 'max_interval'
|
||||
CONF_GPS_ACCURACY_THRESHOLD = 'gps_accuracy_threshold'
|
||||
|
||||
# entity attributes
|
||||
ATTR_ACCOUNTNAME = 'account_name'
|
||||
@@ -64,13 +65,15 @@ DEVICESTATUSCODES = {
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]),
|
||||
vol.Optional(ATTR_DEVICENAME): cv.slugify,
|
||||
vol.Optional(ATTR_INTERVAL): cv.positive_int,
|
||||
vol.Optional(ATTR_INTERVAL): cv.positive_int
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(ATTR_ACCOUNTNAME): cv.slugify,
|
||||
vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int
|
||||
})
|
||||
|
||||
|
||||
@@ -79,8 +82,11 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0]))
|
||||
max_interval = config.get(CONF_MAX_INTERVAL)
|
||||
gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD)
|
||||
|
||||
icloudaccount = Icloud(hass, username, password, account, see)
|
||||
icloudaccount = Icloud(hass, username, password, account, max_interval,
|
||||
gps_accuracy_threshold, see)
|
||||
|
||||
if icloudaccount.api is not None:
|
||||
ICLOUDTRACKERS[account] = icloudaccount
|
||||
@@ -96,6 +102,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].lost_iphone(devicename)
|
||||
|
||||
hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
@@ -106,6 +113,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].update_icloud(devicename)
|
||||
|
||||
hass.services.register(DOMAIN, 'icloud_update', update_icloud,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
@@ -115,6 +123,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].reset_account_icloud()
|
||||
|
||||
hass.services.register(DOMAIN, 'icloud_reset_account',
|
||||
reset_account_icloud, schema=SERVICE_SCHEMA)
|
||||
|
||||
@@ -137,7 +146,8 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
class Icloud(DeviceScanner):
|
||||
"""Representation of an iCloud account."""
|
||||
|
||||
def __init__(self, hass, username, password, name, see):
|
||||
def __init__(self, hass, username, password, name, max_interval,
|
||||
gps_accuracy_threshold, see):
|
||||
"""Initialize an iCloud account."""
|
||||
self.hass = hass
|
||||
self.username = username
|
||||
@@ -148,6 +158,8 @@ class Icloud(DeviceScanner):
|
||||
self.seen_devices = {}
|
||||
self._overridestates = {}
|
||||
self._intervals = {}
|
||||
self._max_interval = max_interval
|
||||
self._gps_accuracy_threshold = gps_accuracy_threshold
|
||||
self.see = see
|
||||
|
||||
self._trusted_device = None
|
||||
@@ -348,7 +360,7 @@ class Icloud(DeviceScanner):
|
||||
self._overridestates[devicename] = None
|
||||
|
||||
if currentzone is not None:
|
||||
self._intervals[devicename] = 30
|
||||
self._intervals[devicename] = self._max_interval
|
||||
return
|
||||
|
||||
if mindistance is None:
|
||||
@@ -363,7 +375,6 @@ class Icloud(DeviceScanner):
|
||||
|
||||
if interval > 180:
|
||||
# Three hour drive? This is far enough that they might be flying
|
||||
# home - check every half hour
|
||||
interval = 30
|
||||
|
||||
if battery is not None and battery <= 33 and mindistance > 3:
|
||||
@@ -403,22 +414,24 @@ class Icloud(DeviceScanner):
|
||||
status = device.status(DEVICESTATUSSET)
|
||||
battery = status.get('batteryLevel', 0) * 100
|
||||
location = status['location']
|
||||
if location:
|
||||
self.determine_interval(
|
||||
devicename, location['latitude'],
|
||||
location['longitude'], battery)
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
attrs[ATTR_INTERVAL] = interval
|
||||
accuracy = location['horizontalAccuracy']
|
||||
kwargs['dev_id'] = dev_id
|
||||
kwargs['host_name'] = status['name']
|
||||
kwargs['gps'] = (location['latitude'],
|
||||
location['longitude'])
|
||||
kwargs['battery'] = battery
|
||||
kwargs['gps_accuracy'] = accuracy
|
||||
kwargs[ATTR_ATTRIBUTES] = attrs
|
||||
self.see(**kwargs)
|
||||
self.seen_devices[devicename] = True
|
||||
if location and location['horizontalAccuracy']:
|
||||
horizontal_accuracy = int(location['horizontalAccuracy'])
|
||||
if horizontal_accuracy < self._gps_accuracy_threshold:
|
||||
self.determine_interval(
|
||||
devicename, location['latitude'],
|
||||
location['longitude'], battery)
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
attrs[ATTR_INTERVAL] = interval
|
||||
accuracy = location['horizontalAccuracy']
|
||||
kwargs['dev_id'] = dev_id
|
||||
kwargs['host_name'] = status['name']
|
||||
kwargs['gps'] = (location['latitude'],
|
||||
location['longitude'])
|
||||
kwargs['battery'] = battery
|
||||
kwargs['gps_accuracy'] = accuracy
|
||||
kwargs[ATTR_ATTRIBUTES] = attrs
|
||||
self.see(**kwargs)
|
||||
self.seen_devices[devicename] = True
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error("No iCloud Devices found")
|
||||
|
||||
@@ -434,7 +447,7 @@ class Icloud(DeviceScanner):
|
||||
device.play_sound()
|
||||
|
||||
def update_icloud(self, devicename=None):
|
||||
"""Authenticate against iCloud and scan for devices."""
|
||||
"""Request device information from iCloud and update device_tracker."""
|
||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
|
||||
if self.api is None:
|
||||
@@ -443,13 +456,13 @@ class Icloud(DeviceScanner):
|
||||
try:
|
||||
if devicename is not None:
|
||||
if devicename in self.devices:
|
||||
self.devices[devicename].location()
|
||||
self.update_device(devicename)
|
||||
else:
|
||||
_LOGGER.error("devicename %s unknown for account %s",
|
||||
devicename, self._attrs[ATTR_ACCOUNTNAME])
|
||||
else:
|
||||
for device in self.devices:
|
||||
self.devices[device].location()
|
||||
self.update_device(device)
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error("No iCloud Devices found")
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user