mirror of
https://github.com/home-assistant/core.git
synced 2026-01-06 23:57:17 +01:00
Compare commits
437 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca973b68e0 | ||
|
|
d19a8ec7da | ||
|
|
4152ac4aa2 | ||
|
|
efdc7042df | ||
|
|
ebf4be3711 | ||
|
|
16d72d2351 | ||
|
|
b2210f429e | ||
|
|
7410bc90f0 | ||
|
|
7e15f179c6 | ||
|
|
321eb2ec6f | ||
|
|
2ee73ca911 | ||
|
|
19a529e917 | ||
|
|
7f065e38a7 | ||
|
|
c4a4802a8c | ||
|
|
44e4f8d1ba | ||
|
|
eaf525d41d | ||
|
|
6d7dbe5536 | ||
|
|
ab397e2b1a | ||
|
|
6be81feb2d | ||
|
|
9b1a75a74b | ||
|
|
8792fd22b9 | ||
|
|
5dd0193ba6 | ||
|
|
b159e8acee | ||
|
|
a99c8eb6c6 | ||
|
|
4218b31e7b | ||
|
|
35bae1eef2 | ||
|
|
d119610cf1 | ||
|
|
4aed41cbe8 | ||
|
|
c462292e4d | ||
|
|
b04e7bba9f | ||
|
|
e6364b4ff6 | ||
|
|
36b9c0a946 | ||
|
|
9086119082 | ||
|
|
dc94079d74 | ||
|
|
78c27b99bd | ||
|
|
f054e9ee54 | ||
|
|
205e83a6d5 | ||
|
|
5063464d5e | ||
|
|
38af04c6ce | ||
|
|
03225cf20f | ||
|
|
3682080da2 | ||
|
|
13cb9cb07b | ||
|
|
05204a982e | ||
|
|
18b288dcfe | ||
|
|
60d7e32f81 | ||
|
|
6a5c7ef43f | ||
|
|
e5c4bba906 | ||
|
|
e07fb24987 | ||
|
|
e7b84432f9 | ||
|
|
b0e062b2f8 | ||
|
|
259121c7a7 | ||
|
|
326241d9d8 | ||
|
|
7c7da9df05 | ||
|
|
cf3f1c3081 | ||
|
|
f00d5cb8ca | ||
|
|
3920de7119 | ||
|
|
fd409a16a1 | ||
|
|
7145afe729 | ||
|
|
70760b5d3b | ||
|
|
d418355d4d | ||
|
|
81ba666db7 | ||
|
|
a147401034 | ||
|
|
36e9f523d1 | ||
|
|
ae257651bf | ||
|
|
53cc3262bd | ||
|
|
2e5b4946e1 | ||
|
|
67c49a7662 | ||
|
|
d06807c634 | ||
|
|
2321603eb7 | ||
|
|
ba20ffdde7 | ||
|
|
cf8907ed0f | ||
|
|
95176b0666 | ||
|
|
f0d9844dfb | ||
|
|
54f8f1223f | ||
|
|
339a839dbe | ||
|
|
e2e10b91a7 | ||
|
|
a9d242a213 | ||
|
|
49581a4a2a | ||
|
|
d8a11fd706 | ||
|
|
d63cf94d6d | ||
|
|
7d8a309017 | ||
|
|
99eeb01525 | ||
|
|
92b07ba8d1 | ||
|
|
c2b06b9e55 | ||
|
|
dd67192057 | ||
|
|
a5ffe0f72b | ||
|
|
7937064fb7 | ||
|
|
0762c7caef | ||
|
|
32b6fb60d8 | ||
|
|
4a85ab1ecb | ||
|
|
981f6fa027 | ||
|
|
125088449a | ||
|
|
fec7c87ff3 | ||
|
|
d333593aa6 | ||
|
|
4e03176634 | ||
|
|
e20e0425b1 | ||
|
|
228b030c82 | ||
|
|
7a979e9f72 | ||
|
|
25c4c9b63c | ||
|
|
03970764d8 | ||
|
|
168e1f0e2d | ||
|
|
d3386907a4 | ||
|
|
de3c76983a | ||
|
|
b9d8789771 | ||
|
|
b186b27600 | ||
|
|
3ca446dda1 | ||
|
|
1bc5042bf9 | ||
|
|
23c39ebefd | ||
|
|
ff83efe376 | ||
|
|
17ba813a6d | ||
|
|
78dd010a04 | ||
|
|
88021ba404 | ||
|
|
491b3d707c | ||
|
|
59141a4063 | ||
|
|
b6805853f1 | ||
|
|
7511286a25 | ||
|
|
70979d855d | ||
|
|
a1010cc63a | ||
|
|
3a2c3fe589 | ||
|
|
eeb9992fde | ||
|
|
191f24b2b9 | ||
|
|
761b4181c0 | ||
|
|
7d8ca2010b | ||
|
|
dbef8f0b78 | ||
|
|
ed85e368e3 | ||
|
|
4867ed23dc | ||
|
|
9f35d4dfca | ||
|
|
b434ffba2d | ||
|
|
a60712d826 | ||
|
|
53078f3069 | ||
|
|
3416d3f5f1 | ||
|
|
b1cc9bf452 | ||
|
|
6fe6dcfac9 | ||
|
|
001515bdc4 | ||
|
|
c1aaef28a9 | ||
|
|
f7e9215f5e | ||
|
|
e2a2fe36fc | ||
|
|
3628fcf083 | ||
|
|
44cad7df30 | ||
|
|
bbd58d7357 | ||
|
|
222748dfbf | ||
|
|
a0eca9c6d1 | ||
|
|
b556b86301 | ||
|
|
e82b358831 | ||
|
|
9658f4383c | ||
|
|
f6c504610b | ||
|
|
f5c633415d | ||
|
|
c5157c1027 | ||
|
|
bba1e2adc9 | ||
|
|
a63714dc86 | ||
|
|
ab74ac8eca | ||
|
|
f19b934869 | ||
|
|
efd155dd3c | ||
|
|
14d052d242 | ||
|
|
39cee987d9 | ||
|
|
6ed765698a | ||
|
|
138350fe3d | ||
|
|
88b7f429c8 | ||
|
|
992516ba86 | ||
|
|
484841c890 | ||
|
|
4e522448b1 | ||
|
|
8645f244da | ||
|
|
be64422d1c | ||
|
|
a0997bd214 | ||
|
|
802bc322e8 | ||
|
|
ed8eda86e2 | ||
|
|
bbc2f1d808 | ||
|
|
83203a10a7 | ||
|
|
113ea2d1dc | ||
|
|
d128f4e51f | ||
|
|
de3a9d552a | ||
|
|
948ef7523e | ||
|
|
9e8340432c | ||
|
|
0a067f4cc7 | ||
|
|
68f92d2e7c | ||
|
|
e14893416f | ||
|
|
9751fed493 | ||
|
|
dc8c331c68 | ||
|
|
d052d45712 | ||
|
|
4242411089 | ||
|
|
f396266c74 | ||
|
|
21d8ecdacd | ||
|
|
6bdb2fe5a0 | ||
|
|
c1c23bb4b6 | ||
|
|
98fef81d19 | ||
|
|
71cab65df6 | ||
|
|
111e515da7 | ||
|
|
f7fc4c6f15 | ||
|
|
a783006f92 | ||
|
|
5b8aeafdb9 | ||
|
|
c1a6131aa8 | ||
|
|
4821858afb | ||
|
|
446390a8d1 | ||
|
|
6a665ffb84 | ||
|
|
10570f5ad6 | ||
|
|
2c1083bda1 | ||
|
|
f8a0a0ba59 | ||
|
|
1d14a17ffd | ||
|
|
a8c9303892 | ||
|
|
bf41674e06 | ||
|
|
6e6ae173fd | ||
|
|
7d5c1581f1 | ||
|
|
6d5fb49687 | ||
|
|
e96ac74b11 | ||
|
|
27b1d448a3 | ||
|
|
84c156e8f5 | ||
|
|
5dcf92fa2a | ||
|
|
6c614df96e | ||
|
|
347ba1a2d8 | ||
|
|
8d0d676ff2 | ||
|
|
781b7687a4 | ||
|
|
286baed9ad | ||
|
|
43ad3ae2d4 | ||
|
|
19d34daef0 | ||
|
|
7c80ef714e | ||
|
|
9ca67c36cd | ||
|
|
6aa8916654 | ||
|
|
2261ce30e3 | ||
|
|
7f5ca314ec | ||
|
|
32166fd56a | ||
|
|
e8173fbc16 | ||
|
|
63552abce5 | ||
|
|
16cb7388ee | ||
|
|
eacfbc048a | ||
|
|
da832dbda2 | ||
|
|
f51a3738aa | ||
|
|
6d431c3fc3 | ||
|
|
2821820281 | ||
|
|
3713dfe139 | ||
|
|
c076b805e7 | ||
|
|
7a44eee093 | ||
|
|
485979cd86 | ||
|
|
5a80d4e5ea | ||
|
|
c3d322f26c | ||
|
|
230b73d14a | ||
|
|
042f292e4f | ||
|
|
daa00bc65a | ||
|
|
1b22f2d8b8 | ||
|
|
1e672b93e7 | ||
|
|
6ee3c1b3e5 | ||
|
|
156206dfee | ||
|
|
7dcb2ae24c | ||
|
|
6b03cb913c | ||
|
|
079724be05 | ||
|
|
f899ce8fbf | ||
|
|
ffd3889271 | ||
|
|
87c69452f9 | ||
|
|
1af65f8f23 | ||
|
|
f0a1beac5d | ||
|
|
4fdbbc497d | ||
|
|
de72eb8fe9 | ||
|
|
2a4971dec7 | ||
|
|
4d7fb2c7de | ||
|
|
184a54cc58 | ||
|
|
c6480e46c4 | ||
|
|
b228695907 | ||
|
|
51c06e35cf | ||
|
|
b8df2d4042 | ||
|
|
2d36d4d9f3 | ||
|
|
03d6071a45 | ||
|
|
6ce9be6b3a | ||
|
|
ed1a883b52 | ||
|
|
f9ee29a5cd | ||
|
|
8d0b7adf24 | ||
|
|
28fec209e0 | ||
|
|
5ad0baf128 | ||
|
|
49d410546a | ||
|
|
722926b315 | ||
|
|
c898fb1f16 | ||
|
|
4f96eeb06e | ||
|
|
316eb59de2 | ||
|
|
5d29d88888 | ||
|
|
210226daac | ||
|
|
f2a2727a15 | ||
|
|
1d8a5147e9 | ||
|
|
3077444d62 | ||
|
|
7829e61361 | ||
|
|
fb985e2909 | ||
|
|
39847ea651 | ||
|
|
17bdcac61b | ||
|
|
e37974c5fc | ||
|
|
46ce114066 | ||
|
|
d68a24b3b8 | ||
|
|
336b00765d | ||
|
|
bb29f16054 | ||
|
|
42ab4e1366 | ||
|
|
722b9ba49b | ||
|
|
f3748cc4fa | ||
|
|
eec3bad94f | ||
|
|
dc21c61a44 | ||
|
|
da9c0a1fd7 | ||
|
|
6f2ee9a34c | ||
|
|
a378e18a3f | ||
|
|
63fcf9d425 | ||
|
|
17b57099ae | ||
|
|
1143499301 | ||
|
|
72fa170265 | ||
|
|
60148f3e83 | ||
|
|
635d36c6ba | ||
|
|
2280dc2a34 | ||
|
|
0d0e0b8ba3 | ||
|
|
a8444b22e7 | ||
|
|
e8d8b75c07 | ||
|
|
02c05e2490 | ||
|
|
92aeef82ef | ||
|
|
909a06566e | ||
|
|
8840c227d2 | ||
|
|
6299c054c8 | ||
|
|
38da81c308 | ||
|
|
6ce9b35e81 | ||
|
|
bf67d0e650 | ||
|
|
fcf97524a2 | ||
|
|
b651cdd8f2 | ||
|
|
6838de3786 | ||
|
|
c9300a98e0 | ||
|
|
de7a4b9501 | ||
|
|
a046e2ed20 | ||
|
|
eaba3b315c | ||
|
|
4837254146 | ||
|
|
371fe9c78f | ||
|
|
22a007a785 | ||
|
|
66dcb6c947 | ||
|
|
fab991bbf6 | ||
|
|
3fd61d8f45 | ||
|
|
e4ef6b91d6 | ||
|
|
dd7bffc28c | ||
|
|
26340fd9df | ||
|
|
fe5626b927 | ||
|
|
b3a47722f0 | ||
|
|
2053c8a908 | ||
|
|
13d6e56106 | ||
|
|
1f041d54d9 | ||
|
|
8d48272cbd | ||
|
|
facd833e6d | ||
|
|
0e2d98dbf5 | ||
|
|
d43a8e593a | ||
|
|
c7c0df53aa | ||
|
|
612dd30201 | ||
|
|
f0d9e5d7ff | ||
|
|
d18709df5b | ||
|
|
f32911d036 | ||
|
|
ad8fe8a93a | ||
|
|
b4dbfe9bbd | ||
|
|
ae32d208d9 | ||
|
|
f5d1f53fab | ||
|
|
96bd153c80 | ||
|
|
7e2e82d956 | ||
|
|
5d4b1ecd3b | ||
|
|
c25c4c85d6 | ||
|
|
78c44180f4 | ||
|
|
416f64fc70 | ||
|
|
f25d56d666 | ||
|
|
6f043f3c5c | ||
|
|
6500cb7915 | ||
|
|
d85ed8d0fe | ||
|
|
c82ca62820 | ||
|
|
28964806c5 | ||
|
|
c414ecd4f0 | ||
|
|
72f100723f | ||
|
|
9e4da37022 | ||
|
|
e5f000f976 | ||
|
|
e2408cc804 | ||
|
|
9bfeb3b5af | ||
|
|
c5c409bed3 | ||
|
|
8bff813014 | ||
|
|
a4944da68f | ||
|
|
bc64053214 | ||
|
|
c7416c8986 | ||
|
|
429628ec1d | ||
|
|
16dafaa5af | ||
|
|
f0231c1f29 | ||
|
|
5995c2f313 | ||
|
|
80d2c76e85 | ||
|
|
d2cea84254 | ||
|
|
a4b88fc31b | ||
|
|
f5c2e7ff68 | ||
|
|
00ff305bd7 | ||
|
|
66d14da5e9 | ||
|
|
ba9fef4de6 | ||
|
|
0a558a0e82 | ||
|
|
2c202690d8 | ||
|
|
04bde68db3 | ||
|
|
2d77a2bb39 | ||
|
|
52f57b755e | ||
|
|
870728f68f | ||
|
|
48f40453f7 | ||
|
|
073126755c | ||
|
|
034eb9ae1a | ||
|
|
d34a4fb6e3 | ||
|
|
7a9ceb6f54 | ||
|
|
a06000c76d | ||
|
|
2a0bd8d330 | ||
|
|
ead158b68c | ||
|
|
bfd9a5a863 | ||
|
|
56b185f7ab | ||
|
|
34ccfae565 | ||
|
|
dc8a0205ee | ||
|
|
7471211b60 | ||
|
|
04b68902e3 | ||
|
|
ebe4418afe | ||
|
|
eaa2791539 | ||
|
|
7059b6c6c1 | ||
|
|
5d15b257c4 | ||
|
|
669929de06 | ||
|
|
eb7adc74ef | ||
|
|
3b3050434a | ||
|
|
7e9dcfa4c9 | ||
|
|
c193d80ec5 | ||
|
|
2e3524147c | ||
|
|
f28fa7447e | ||
|
|
069454323e | ||
|
|
ed1d6f1027 | ||
|
|
28ed304c93 | ||
|
|
3e150bb2b3 | ||
|
|
247edf1b69 | ||
|
|
219ed7331c | ||
|
|
47bfef9640 | ||
|
|
767d3c6206 | ||
|
|
e4a826d1c1 | ||
|
|
a71d5f4614 | ||
|
|
6c358fa6a3 | ||
|
|
26209de2f2 | ||
|
|
678f284015 | ||
|
|
64c5d26a84 | ||
|
|
2edebfee0a | ||
|
|
b1c0cabe6c | ||
|
|
17e5740a0c | ||
|
|
8b9eab196c | ||
|
|
65c6f72c9d | ||
|
|
fe1a85047e | ||
|
|
74010fc2df | ||
|
|
a9e2dd3427 | ||
|
|
f2296e1ff8 | ||
|
|
134445f622 | ||
|
|
cad9e9a4cb | ||
|
|
1db4df6d3a | ||
|
|
7c36c5d9b4 |
39
.coveragerc
39
.coveragerc
@@ -29,6 +29,9 @@ omit =
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
homeassistant/components/bmw_connected_drive.py
|
||||
homeassistant/components/*/bmw_connected_drive.py
|
||||
|
||||
homeassistant/components/android_ip_webcam.py
|
||||
homeassistant/components/*/android_ip_webcam.py
|
||||
|
||||
@@ -38,6 +41,9 @@ omit =
|
||||
homeassistant/components/asterisk_mbox.py
|
||||
homeassistant/components/*/asterisk_mbox.py
|
||||
|
||||
homeassistant/components/august.py
|
||||
homeassistant/components/*/august.py
|
||||
|
||||
homeassistant/components/axis.py
|
||||
homeassistant/components/*/axis.py
|
||||
|
||||
@@ -56,6 +62,9 @@ omit =
|
||||
homeassistant/components/comfoconnect.py
|
||||
homeassistant/components/*/comfoconnect.py
|
||||
|
||||
homeassistant/components/daikin.py
|
||||
homeassistant/components/*/daikin.py
|
||||
|
||||
homeassistant/components/deconz/*
|
||||
homeassistant/components/*/deconz.py
|
||||
|
||||
@@ -76,6 +85,9 @@ omit =
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/egardia.py
|
||||
homeassistant/components/*/egardia.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
@@ -175,6 +187,9 @@ omit =
|
||||
homeassistant/components/opencv.py
|
||||
homeassistant/components/*/opencv.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/qwikswitch.py
|
||||
homeassistant/components/*/qwikswitch.py
|
||||
|
||||
@@ -205,6 +220,9 @@ omit =
|
||||
homeassistant/components/skybell.py
|
||||
homeassistant/components/*/skybell.py
|
||||
|
||||
homeassistant/components/smappee.py
|
||||
homeassistant/components/*/smappee.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
@@ -235,6 +253,9 @@ omit =
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
homeassistant/components/upcloud.py
|
||||
homeassistant/components/*/upcloud.py
|
||||
|
||||
homeassistant/components/usps.py
|
||||
homeassistant/components/*/usps.py
|
||||
|
||||
@@ -284,13 +305,9 @@ omit =
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/daikin.py
|
||||
homeassistant/components/*/daikin.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/egardia.py
|
||||
homeassistant/components/alarm_control_panel/ialarm.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
@@ -303,7 +320,6 @@ omit =
|
||||
homeassistant/components/binary_sensor/hikvision.py
|
||||
homeassistant/components/binary_sensor/iss.py
|
||||
homeassistant/components/binary_sensor/mystrom.py
|
||||
homeassistant/components/binary_sensor/pilight.py
|
||||
homeassistant/components/binary_sensor/ping.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
@@ -344,12 +360,14 @@ omit =
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/automatic.py
|
||||
homeassistant/components/device_tracker/bbox.py
|
||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/hitron_coda.py
|
||||
@@ -455,6 +473,7 @@ omit =
|
||||
homeassistant/components/media_player/russound_rio.py
|
||||
homeassistant/components/media_player/russound_rnet.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/songpal.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
@@ -462,6 +481,7 @@ omit =
|
||||
homeassistant/components/media_player/vizio.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/xiaomi_tv.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/media_player/yamaha_musiccast.py
|
||||
homeassistant/components/media_player/ziggo_mediabox_xl.py
|
||||
@@ -498,6 +518,7 @@ omit =
|
||||
homeassistant/components/notify/simplepush.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/synology_chat.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/telstra.py
|
||||
@@ -551,8 +572,10 @@ omit =
|
||||
homeassistant/components/sensor/etherscan.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/filesize.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gearbest.py
|
||||
@@ -607,16 +630,19 @@ omit =
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/shodan.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/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/spotcrime.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
@@ -626,7 +652,6 @@ omit =
|
||||
homeassistant/components/sensor/sytadin.py
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/teksavvy.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/tibber.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
@@ -645,6 +670,7 @@ omit =
|
||||
homeassistant/components/sensor/worxlandroid.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/sensor/zestimate.py
|
||||
homeassistant/components/shiftr.py
|
||||
homeassistant/components/spc.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
@@ -662,7 +688,6 @@ omit =
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pilight.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rainbird.py
|
||||
homeassistant/components/switch/rainmachine.py
|
||||
|
||||
11
.gitattributes
vendored
11
.gitattributes
vendored
@@ -1,3 +1,10 @@
|
||||
# Ensure Docker script files uses LF to support Docker for Windows.
|
||||
setup_docker_prereqs eol=lf
|
||||
/virtualization/Docker/scripts/* eol=lf
|
||||
# Ensure "git config --global core.autocrlf input" before you clone
|
||||
* text eol=lf
|
||||
*.py whitespace=error
|
||||
|
||||
*.ico binary
|
||||
*.jpg binary
|
||||
*.png binary
|
||||
*.zip binary
|
||||
*.mp3 binary
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -103,3 +103,6 @@ desktop.ini
|
||||
|
||||
# mypy
|
||||
/.mypy_cache/*
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
|
||||
17
.travis.yml
17
.travis.yml
@@ -6,12 +6,10 @@ addons:
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.4.2"
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=lint
|
||||
- python: "3.4.2"
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=pylint
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=py34
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
@@ -30,4 +28,15 @@ cache:
|
||||
install: pip install -U tox coveralls
|
||||
language: python
|
||||
script: travis_wait 30 tox --develop
|
||||
services:
|
||||
- docker
|
||||
before_deploy:
|
||||
- docker pull lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7
|
||||
deploy:
|
||||
skip_cleanup: true
|
||||
provider: script
|
||||
script: script/travis_deploy
|
||||
on:
|
||||
branch: dev
|
||||
condition: $TOXENV = lint
|
||||
after_success: coveralls
|
||||
|
||||
11
CODEOWNERS
11
CODEOWNERS
@@ -43,6 +43,8 @@ homeassistant/components/hassio.py @home-assistant/hassio
|
||||
|
||||
# Individual components
|
||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
||||
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
@@ -53,8 +55,12 @@ homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
homeassistant/components/media_player/emby.py @mezz64
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/mediaroom.py @dgomes
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
homeassistant/components/media_player/sonos.py @amelchio
|
||||
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
|
||||
@@ -63,6 +69,7 @@ homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/sql.py @dgomes
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/switch/rainmachine.py @bachya
|
||||
@@ -70,9 +77,13 @@ homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
homeassistant/components/*/axis.py @kane610
|
||||
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/eight_sleep.py @mezz64
|
||||
homeassistant/components/*/eight_sleep.py @mezz64
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
|
||||
@@ -22,10 +22,23 @@ import os
|
||||
import inspect
|
||||
|
||||
from homeassistant.const import __version__, __short_version__
|
||||
from setup import (
|
||||
PROJECT_NAME, PROJECT_LONG_DESCRIPTION, PROJECT_COPYRIGHT, PROJECT_AUTHOR,
|
||||
PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY, GITHUB_PATH,
|
||||
GITHUB_URL)
|
||||
|
||||
PROJECT_NAME = 'Home Assistant'
|
||||
PROJECT_PACKAGE_NAME = 'homeassistant'
|
||||
PROJECT_AUTHOR = 'The Home Assistant Authors'
|
||||
PROJECT_COPYRIGHT = ' 2013-2018, {}'.format(PROJECT_AUTHOR)
|
||||
PROJECT_LONG_DESCRIPTION = ('Home Assistant is an open-source '
|
||||
'home automation platform running on Python 3. '
|
||||
'Track and control all devices at home and '
|
||||
'automate control. '
|
||||
'Installation in less than a minute.')
|
||||
PROJECT_GITHUB_USERNAME = 'home-assistant'
|
||||
PROJECT_GITHUB_REPOSITORY = 'home-assistant'
|
||||
|
||||
GITHUB_PATH = '{}/{}'.format(
|
||||
PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY)
|
||||
GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH)
|
||||
|
||||
|
||||
sys.path.insert(0, os.path.abspath('_ext'))
|
||||
sys.path.insert(0, os.path.abspath('../homeassistant'))
|
||||
|
||||
@@ -15,7 +15,6 @@ from homeassistant.const import (
|
||||
__version__,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
REQUIRED_PYTHON_VER,
|
||||
REQUIRED_PYTHON_VER_WIN,
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
|
||||
@@ -33,12 +32,7 @@ def attempt_use_uvloop():
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate that the right Python version is running."""
|
||||
if sys.platform == "win32" and \
|
||||
sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER_WIN))
|
||||
sys.exit(1)
|
||||
elif sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER))
|
||||
sys.exit(1)
|
||||
|
||||
@@ -12,7 +12,8 @@ from typing import Any, Optional, Dict
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import (
|
||||
core, config as conf_util, loader, components as core_components)
|
||||
core, config as conf_util, config_entries, loader,
|
||||
components as core_components)
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -35,13 +36,13 @@ FIRST_INIT_COMPONENT = set((
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
hass: Optional[core.HomeAssistant]=None,
|
||||
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) \
|
||||
hass: Optional[core.HomeAssistant] = None,
|
||||
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) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
@@ -68,12 +69,12 @@ def from_config_dict(config: Dict[str, Any],
|
||||
@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) \
|
||||
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) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
@@ -111,21 +112,20 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
if not loader.PREPARED:
|
||||
yield from hass.async_add_job(loader.prepare, hass)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
config = OrderedDict(config)
|
||||
|
||||
# Merge packages
|
||||
conf_util.merge_packages_config(
|
||||
config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
# Use OrderedDict in case original one was one.
|
||||
# Convert values to dictionaries if they are None
|
||||
new_config = OrderedDict()
|
||||
for key, value in config.items():
|
||||
new_config[key] = value or {}
|
||||
config = new_config
|
||||
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
||||
yield from hass.config_entries.async_load()
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
components.update(hass.config_entries.async_domains())
|
||||
|
||||
# setup components
|
||||
# pylint: disable=not-an-iterable
|
||||
@@ -163,11 +163,11 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
|
||||
|
||||
def from_config_file(config_path: str,
|
||||
hass: Optional[core.HomeAssistant]=None,
|
||||
verbose: bool=False,
|
||||
skip_pip: bool=True,
|
||||
log_rotate_days: Any=None,
|
||||
log_file: Any=None):
|
||||
hass: Optional[core.HomeAssistant] = None,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
@@ -188,10 +188,10 @@ def from_config_file(config_path: str,
|
||||
@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):
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter.
|
||||
@@ -219,7 +219,7 @@ def async_from_config_file(config_path: str,
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
|
||||
log_rotate_days=None, log_file=None) -> None:
|
||||
"""Set up the logging.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
||||
@@ -154,6 +155,13 @@ def async_setup(hass, config):
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF,
|
||||
"Turned {} off"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_core_service(call):
|
||||
|
||||
@@ -7,6 +7,7 @@ https://home-assistant.io/components/abode/
|
||||
import asyncio
|
||||
import logging
|
||||
from functools import partial
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -17,7 +18,6 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.12.2']
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.0']
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -59,8 +59,7 @@ class CanaryAlarm(AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode.name == LOCATION_MODE_NIGHT:
|
||||
return STATE_ALARM_ARMED_NIGHT
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
||||
@@ -26,7 +26,7 @@ DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=1)
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
|
||||
@@ -4,130 +4,65 @@ Interfaces with Egardia/Woonveilig alarm control panel.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.egardia/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN)
|
||||
import homeassistant.exceptions as exc
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.26']
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE, EGARDIA_SERVER,
|
||||
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
|
||||
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
|
||||
)
|
||||
REQUIREMENTS = ['pythonegardia==1.0.38']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_REPORT_SERVER_CODES = 'report_server_codes'
|
||||
CONF_REPORT_SERVER_ENABLED = 'report_server_enabled'
|
||||
CONF_REPORT_SERVER_PORT = 'report_server_port'
|
||||
CONF_REPORT_SERVER_CODES_IGNORE = 'ignore'
|
||||
CONF_VERSION = 'version'
|
||||
|
||||
DEFAULT_NAME = 'Egardia'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_REPORT_SERVER_ENABLED = False
|
||||
DEFAULT_REPORT_SERVER_PORT = 52010
|
||||
DEFAULT_VERSION = 'GATE-01'
|
||||
DOMAIN = 'egardia'
|
||||
D_EGARDIASRV = 'egardiaserver'
|
||||
|
||||
NOTIFICATION_ID = 'egardia_notification'
|
||||
NOTIFICATION_TITLE = 'Egardia'
|
||||
|
||||
STATES = {
|
||||
'ARM': STATE_ALARM_ARMED_AWAY,
|
||||
'DAY HOME': STATE_ALARM_ARMED_HOME,
|
||||
'DISARM': STATE_ALARM_DISARMED,
|
||||
'HOME': STATE_ALARM_ARMED_HOME,
|
||||
'TRIGGERED': STATE_ALARM_TRIGGERED,
|
||||
'UNKNOWN': STATE_UNKNOWN,
|
||||
'ARMHOME': STATE_ALARM_ARMED_HOME,
|
||||
'TRIGGERED': STATE_ALARM_TRIGGERED
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_REPORT_SERVER_CODES): vol.All(cv.ensure_list),
|
||||
vol.Optional(CONF_REPORT_SERVER_ENABLED,
|
||||
default=DEFAULT_REPORT_SERVER_ENABLED): cv.boolean,
|
||||
vol.Optional(CONF_REPORT_SERVER_PORT, default=DEFAULT_REPORT_SERVER_PORT):
|
||||
cv.port,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Egardia platform."""
|
||||
from pythonegardia import egardiadevice
|
||||
from pythonegardia import egardiaserver
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
rs_enabled = config.get(CONF_REPORT_SERVER_ENABLED)
|
||||
rs_port = config.get(CONF_REPORT_SERVER_PORT)
|
||||
rs_codes = config.get(CONF_REPORT_SERVER_CODES)
|
||||
version = config.get(CONF_VERSION)
|
||||
|
||||
try:
|
||||
egardiasystem = egardiadevice.EgardiaDevice(
|
||||
host, port, username, password, '', version)
|
||||
except requests.exceptions.RequestException:
|
||||
raise exc.PlatformNotReady()
|
||||
except egardiadevice.UnauthorizedError:
|
||||
_LOGGER.error("Unable to authorize. Wrong password or username")
|
||||
return
|
||||
|
||||
eg_dev = EgardiaAlarm(
|
||||
name, egardiasystem, rs_enabled, rs_codes)
|
||||
|
||||
if rs_enabled:
|
||||
# Set up the egardia server
|
||||
_LOGGER.info("Setting up EgardiaServer")
|
||||
try:
|
||||
if D_EGARDIASRV not in hass.data:
|
||||
server = egardiaserver.EgardiaServer('', rs_port)
|
||||
bound = server.bind()
|
||||
if not bound:
|
||||
raise IOError(
|
||||
"Binding error occurred while starting EgardiaServer")
|
||||
hass.data[D_EGARDIASRV] = server
|
||||
server.start()
|
||||
except IOError:
|
||||
return
|
||||
hass.data[D_EGARDIASRV].register_callback(eg_dev.handle_status_event)
|
||||
|
||||
def handle_stop_event(event):
|
||||
"""Call function for Home Assistant stop event."""
|
||||
hass.data[D_EGARDIASRV].stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event)
|
||||
|
||||
add_devices([eg_dev], True)
|
||||
device = EgardiaAlarm(
|
||||
discovery_info['name'],
|
||||
hass.data[EGARDIA_DEVICE],
|
||||
discovery_info[CONF_REPORT_SERVER_ENABLED],
|
||||
discovery_info.get(CONF_REPORT_SERVER_CODES),
|
||||
discovery_info[CONF_REPORT_SERVER_PORT])
|
||||
# add egardia alarm device
|
||||
add_devices([device], True)
|
||||
|
||||
|
||||
class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a Egardia alarm."""
|
||||
|
||||
def __init__(self, name, egardiasystem, rs_enabled=False, rs_codes=None):
|
||||
def __init__(self, name, egardiasystem,
|
||||
rs_enabled=False, rs_codes=None, rs_port=52010):
|
||||
"""Initialize the Egardia alarm."""
|
||||
self._name = name
|
||||
self._egardiasystem = egardiasystem
|
||||
self._status = None
|
||||
self._rs_enabled = rs_enabled
|
||||
if rs_codes is not None:
|
||||
self._rs_codes = rs_codes[0]
|
||||
else:
|
||||
self._rs_codes = rs_codes
|
||||
self._rs_codes = rs_codes
|
||||
self._rs_port = rs_port
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add Egardiaserver callback if enabled."""
|
||||
if self._rs_enabled:
|
||||
_LOGGER.debug("Registering callback to Egardiaserver")
|
||||
self.hass.data[EGARDIA_SERVER].register_callback(
|
||||
self.handle_status_event)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -156,31 +91,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def lookupstatusfromcode(self, statuscode):
|
||||
"""Look at the rs_codes and returns the status from the code."""
|
||||
status = 'UNKNOWN'
|
||||
if self._rs_codes is not None:
|
||||
statuscode = str(statuscode).strip()
|
||||
for i in self._rs_codes:
|
||||
val = str(self._rs_codes[i]).strip()
|
||||
if ',' in val:
|
||||
splitted = val.split(',')
|
||||
for code in splitted:
|
||||
code = str(code).strip()
|
||||
if statuscode == code:
|
||||
status = i.upper()
|
||||
break
|
||||
elif statuscode == val:
|
||||
status = i.upper()
|
||||
break
|
||||
status = next((
|
||||
status_group.upper() for status_group, codes
|
||||
in self._rs_codes.items() for code in codes
|
||||
if statuscode == code), 'UNKNOWN')
|
||||
return status
|
||||
|
||||
def parsestatus(self, status):
|
||||
"""Parse the status."""
|
||||
_LOGGER.debug("Parsing status %s", status)
|
||||
# Ignore the statuscode if it is IGNORE
|
||||
if status.lower().strip() != CONF_REPORT_SERVER_CODES_IGNORE:
|
||||
_LOGGER.debug("Not ignoring status")
|
||||
newstatus = ([v for k, v in STATES.items()
|
||||
if status.upper() == k][0])
|
||||
if status.lower().strip() != REPORT_SERVER_CODES_IGNORE:
|
||||
_LOGGER.debug("Not ignoring status %s", status)
|
||||
newstatus = STATES.get(status.upper())
|
||||
_LOGGER.debug("newstatus %s", newstatus)
|
||||
self._status = newstatus
|
||||
else:
|
||||
_LOGGER.error("Ignoring status")
|
||||
|
||||
@@ -172,9 +172,8 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
self._within_pending_time(self._state):
|
||||
@@ -187,8 +186,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
"""Get the current state."""
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
else:
|
||||
return self._state
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
"""Get the pending time."""
|
||||
|
||||
@@ -208,9 +208,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
self._within_pending_time(self._state):
|
||||
@@ -223,8 +222,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
"""Get the current state."""
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
else:
|
||||
return self._state
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
"""Get the pending time."""
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
# Describes the format for available alarm control panel services
|
||||
|
||||
alarm_disarm:
|
||||
description: Send the alarm the command for disarm.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to disarm.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to disarm the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_arm_home:
|
||||
description: Send the alarm the command for arm home.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm home.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm home the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_arm_away:
|
||||
description: Send the alarm the command for arm away.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm away.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm away the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_arm_night:
|
||||
description: Send the alarm the command for arm night.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm night.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm night the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_trigger:
|
||||
description: Send the alarm the command for trigger.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
envisalink_alarm_keypress:
|
||||
description: Send custom keypresses to the alarm.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel (1-6 characters).'
|
||||
example: '*71'
|
||||
|
||||
alarmdecoder_alarm_toggle_chime:
|
||||
description: Send the alarm the toggle chime command.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: A required code to toggle the alarm control panel chime with.
|
||||
example: 1234
|
||||
# Describes the format for available alarm control panel services
|
||||
|
||||
alarm_disarm:
|
||||
description: Send the alarm the command for disarm.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to disarm.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to disarm the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_arm_home:
|
||||
description: Send the alarm the command for arm home.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm home.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm home the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_arm_away:
|
||||
description: Send the alarm the command for arm away.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm away.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm away the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_arm_night:
|
||||
description: Send the alarm the command for arm night.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm night.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm night the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
alarm_trigger:
|
||||
description: Send the alarm the command for trigger.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
envisalink_alarm_keypress:
|
||||
description: Send custom keypresses to the alarm.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel (1-6 characters).'
|
||||
example: '*71'
|
||||
|
||||
alarmdecoder_alarm_toggle_chime:
|
||||
description: Send the alarm the toggle chime command.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: A required code to toggle the alarm control panel chime with.
|
||||
example: 1234
|
||||
|
||||
@@ -34,7 +34,7 @@ DEFAULT_SKIP_FIRST = False
|
||||
|
||||
ALERT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DONE_MESSAGE, default=None): cv.string,
|
||||
vol.Optional(CONF_DONE_MESSAGE): cv.string,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
||||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||
@@ -121,7 +121,7 @@ def async_setup(hass, config):
|
||||
# Setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
entity = Alert(hass, entity_id,
|
||||
alert[CONF_NAME], alert[CONF_DONE_MESSAGE],
|
||||
alert[CONF_NAME], alert.get(CONF_DONE_MESSAGE),
|
||||
alert[CONF_ENTITY_ID], alert[CONF_STATE],
|
||||
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
|
||||
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
|
||||
|
||||
@@ -31,10 +31,7 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
SMART_HOME_SCHEMA = vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_FILTER,
|
||||
default=lambda: entityfilter.generate_filter([], [], [], [])
|
||||
): entityfilter.FILTER_SCHEMA,
|
||||
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
|
||||
})
|
||||
|
||||
|
||||
@@ -328,8 +328,9 @@ class _AlexaBrightnessController(_AlexaInterface):
|
||||
def get_property(self, name):
|
||||
if name != 'brightness':
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
return round(self.entity.attributes['brightness'] / 255.0 * 100)
|
||||
if 'brightness' in self.entity.attributes:
|
||||
return round(self.entity.attributes['brightness'] / 255.0 * 100)
|
||||
return 0
|
||||
|
||||
|
||||
class _AlexaColorController(_AlexaInterface):
|
||||
@@ -390,6 +391,7 @@ class _AlexaTemperatureSensor(_AlexaInterface):
|
||||
|
||||
@ENTITY_ADAPTERS.register(alert.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(automation.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(group.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
|
||||
class _GenericCapabilities(_AlexaEntity):
|
||||
"""A generic, on/off device.
|
||||
@@ -520,16 +522,6 @@ class _ScriptCapabilities(_AlexaEntity):
|
||||
supports_deactivation=can_cancel)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(group.DOMAIN)
|
||||
class _GroupCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.SCENE_TRIGGER]
|
||||
|
||||
def interfaces(self):
|
||||
return [_AlexaSceneController(self.entity,
|
||||
supports_deactivation=True)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
|
||||
class _SensorCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
@@ -772,6 +764,8 @@ def extract_entity(funct):
|
||||
def async_api_turn_on(hass, config, request, entity):
|
||||
"""Process a turn on request."""
|
||||
domain = entity.domain
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
service = SERVICE_TURN_ON
|
||||
if entity.domain == cover.DOMAIN:
|
||||
@@ -927,10 +921,7 @@ def async_api_increase_color_temp(hass, config, request, entity):
|
||||
@asyncio.coroutine
|
||||
def async_api_activate(hass, config, request, entity):
|
||||
"""Process an activate request."""
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
else:
|
||||
domain = entity.domain
|
||||
domain = entity.domain
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
@@ -954,10 +945,7 @@ def async_api_activate(hass, config, request, entity):
|
||||
@asyncio.coroutine
|
||||
def async_api_deactivate(hass, config, request, entity):
|
||||
"""Process a deactivate request."""
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
else:
|
||||
domain = entity.domain
|
||||
domain = entity.domain
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
@@ -1064,7 +1052,16 @@ def async_api_lock(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
# Alexa expects a lockState in the response, we don't know the actual
|
||||
# lockState at this point but assume it is locked. It is reported
|
||||
# correctly later when ReportState is called. The alt. to this approach
|
||||
# is to implement DeferredResponse
|
||||
properties = [{
|
||||
'name': 'lockState',
|
||||
'namespace': 'Alexa.LockController',
|
||||
'value': 'LOCKED'
|
||||
}]
|
||||
return api_message(request, context={'properties': properties})
|
||||
|
||||
|
||||
# Not supported by Alexa yet
|
||||
@@ -1168,20 +1165,24 @@ def async_api_adjust_volume(hass, config, request, entity):
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_volume_step(hass, config, request, entity):
|
||||
"""Process an adjust volume step request."""
|
||||
volume_step = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
|
||||
|
||||
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
||||
|
||||
volume = current_level + volume_step
|
||||
# media_player volume up/down service does not support specifying steps
|
||||
# each component handles it differently e.g. via config.
|
||||
# For now we use the volumeSteps returned to figure out if we
|
||||
# should step up/down
|
||||
volume_step = request[API_PAYLOAD]['volumeSteps']
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=False)
|
||||
if volume_step > 0:
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_UP,
|
||||
data, blocking=False)
|
||||
elif volume_step < 0:
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_DOWN,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -140,11 +140,11 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
cv.time_period,
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_SWITCHES, default=None):
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(CONF_MOTION_SENSOR, default=None): cv.boolean,
|
||||
vol.Optional(CONF_MOTION_SENSOR): cv.boolean,
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -165,9 +165,9 @@ def async_setup(hass, config):
|
||||
password = cam_config.get(CONF_PASSWORD)
|
||||
name = cam_config[CONF_NAME]
|
||||
interval = cam_config[CONF_SCAN_INTERVAL]
|
||||
switches = cam_config[CONF_SWITCHES]
|
||||
sensors = cam_config[CONF_SENSORS]
|
||||
motion = cam_config[CONF_MOTION_SENSOR]
|
||||
switches = cam_config.get(CONF_SWITCHES)
|
||||
sensors = cam_config.get(CONF_SENSORS)
|
||||
motion = cam_config.get(CONF_MOTION_SENSOR)
|
||||
|
||||
# Init ip webcam
|
||||
cam = PyDroidIPCam(
|
||||
|
||||
@@ -131,8 +131,7 @@ class APIEventStream(HomeAssistantView):
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
response.write(msg.encode("UTF-8"))
|
||||
yield from response.drain()
|
||||
yield from response.write(msg.encode("UTF-8"))
|
||||
except asyncio.TimeoutError:
|
||||
yield from to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||
vol.Optional(CONF_CREDENTIALS, default=None): cv.string,
|
||||
vol.Optional(CONF_CREDENTIALS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean,
|
||||
})])
|
||||
|
||||
257
homeassistant/components/august.py
Normal file
257
homeassistant/components/august.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Support for August devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/august/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
from requests import RequestException
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_CONFIGURING = {}
|
||||
|
||||
REQUIREMENTS = ['py-august==0.4.0']
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
ACTIVITY_FETCH_LIMIT = 10
|
||||
ACTIVITY_INITIAL_FETCH_LIMIT = 20
|
||||
|
||||
CONF_LOGIN_METHOD = 'login_method'
|
||||
CONF_INSTALL_ID = 'install_id'
|
||||
|
||||
NOTIFICATION_ID = 'august_notification'
|
||||
NOTIFICATION_TITLE = "August Setup"
|
||||
|
||||
AUGUST_CONFIG_FILE = '.august.conf'
|
||||
|
||||
DATA_AUGUST = 'august'
|
||||
DOMAIN = 'august'
|
||||
DEFAULT_ENTITY_NAMESPACE = 'august'
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
|
||||
LOGIN_METHODS = ['phone', 'email']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_INSTALL_ID): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
AUGUST_COMPONENTS = [
|
||||
'camera', 'binary_sensor', 'lock'
|
||||
]
|
||||
|
||||
|
||||
def request_configuration(hass, config, api, authenticator):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
def august_configuration_callback(data):
|
||||
"""Run when the configuration callback is called."""
|
||||
from august.authenticator import ValidationResult
|
||||
|
||||
result = authenticator.validate_verification_code(
|
||||
data.get('verification_code'))
|
||||
|
||||
if result == ValidationResult.INVALID_VERIFICATION_CODE:
|
||||
configurator.notify_errors(_CONFIGURING[DOMAIN],
|
||||
"Invalid verification code")
|
||||
elif result == ValidationResult.VALIDATED:
|
||||
setup_august(hass, config, api, authenticator)
|
||||
|
||||
if DOMAIN not in _CONFIGURING:
|
||||
authenticator.send_verification_code()
|
||||
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
login_method = conf.get(CONF_LOGIN_METHOD)
|
||||
|
||||
_CONFIGURING[DOMAIN] = configurator.request_config(
|
||||
NOTIFICATION_TITLE,
|
||||
august_configuration_callback,
|
||||
description="Please check your {} ({}) and enter the verification "
|
||||
"code below".format(login_method, username),
|
||||
submit_caption='Verify',
|
||||
fields=[{
|
||||
'id': 'verification_code',
|
||||
'name': "Verification code",
|
||||
'type': 'string'}]
|
||||
)
|
||||
|
||||
|
||||
def setup_august(hass, config, api, authenticator):
|
||||
"""Set up the August component."""
|
||||
from august.authenticator import AuthenticationState
|
||||
|
||||
authentication = None
|
||||
try:
|
||||
authentication = authenticator.authenticate()
|
||||
except RequestException as ex:
|
||||
_LOGGER.error("Unable to connect to August service: %s", str(ex))
|
||||
|
||||
hass.components.persistent_notification.create(
|
||||
"Error: {}<br />"
|
||||
"You will need to restart hass after fixing."
|
||||
"".format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
|
||||
state = authentication.state
|
||||
|
||||
if state == AuthenticationState.AUTHENTICATED:
|
||||
if DOMAIN in _CONFIGURING:
|
||||
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
|
||||
|
||||
hass.data[DATA_AUGUST] = AugustData(api, authentication.access_token)
|
||||
|
||||
for component in AUGUST_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
elif state == AuthenticationState.BAD_PASSWORD:
|
||||
return False
|
||||
elif state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
request_configuration(hass, config, api, authenticator)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the August component."""
|
||||
from august.api import Api
|
||||
from august.authenticator import Authenticator
|
||||
|
||||
conf = config[DOMAIN]
|
||||
api = Api(timeout=conf.get(CONF_TIMEOUT))
|
||||
|
||||
authenticator = Authenticator(
|
||||
api,
|
||||
conf.get(CONF_LOGIN_METHOD),
|
||||
conf.get(CONF_USERNAME),
|
||||
conf.get(CONF_PASSWORD),
|
||||
install_id=conf.get(CONF_INSTALL_ID),
|
||||
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE))
|
||||
|
||||
return setup_august(hass, config, api, authenticator)
|
||||
|
||||
|
||||
class AugustData:
|
||||
"""August data object."""
|
||||
|
||||
def __init__(self, api, access_token):
|
||||
"""Init August data object."""
|
||||
self._api = api
|
||||
self._access_token = access_token
|
||||
self._doorbells = self._api.get_doorbells(self._access_token) or []
|
||||
self._locks = self._api.get_operable_locks(self._access_token) or []
|
||||
self._house_ids = [d.house_id for d in self._doorbells + self._locks]
|
||||
|
||||
self._doorbell_detail_by_id = {}
|
||||
self._lock_status_by_id = {}
|
||||
self._lock_detail_by_id = {}
|
||||
self._activities_by_id = {}
|
||||
|
||||
@property
|
||||
def house_ids(self):
|
||||
"""Return a list of house_ids."""
|
||||
return self._house_ids
|
||||
|
||||
@property
|
||||
def doorbells(self):
|
||||
"""Return a list of doorbells."""
|
||||
return self._doorbells
|
||||
|
||||
@property
|
||||
def locks(self):
|
||||
"""Return a list of locks."""
|
||||
return self._locks
|
||||
|
||||
def get_device_activities(self, device_id, *activity_types):
|
||||
"""Return a list of activities."""
|
||||
self._update_device_activities()
|
||||
|
||||
activities = self._activities_by_id.get(device_id, [])
|
||||
if activity_types:
|
||||
return [a for a in activities if a.activity_type in activity_types]
|
||||
return activities
|
||||
|
||||
def get_latest_device_activity(self, device_id, *activity_types):
|
||||
"""Return latest activity."""
|
||||
activities = self.get_device_activities(device_id, *activity_types)
|
||||
return next(iter(activities or []), None)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||
"""Update data object with latest from August API."""
|
||||
for house_id in self.house_ids:
|
||||
activities = self._api.get_house_activities(self._access_token,
|
||||
house_id,
|
||||
limit=limit)
|
||||
|
||||
device_ids = {a.device_id for a in activities}
|
||||
for device_id in device_ids:
|
||||
self._activities_by_id[device_id] = [a for a in activities if
|
||||
a.device_id == device_id]
|
||||
|
||||
def get_doorbell_detail(self, doorbell_id):
|
||||
"""Return doorbell detail."""
|
||||
self._update_doorbells()
|
||||
return self._doorbell_detail_by_id.get(doorbell_id)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_doorbells(self):
|
||||
detail_by_id = {}
|
||||
|
||||
for doorbell in self._doorbells:
|
||||
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
||||
self._access_token, doorbell.device_id)
|
||||
|
||||
self._doorbell_detail_by_id = detail_by_id
|
||||
|
||||
def get_lock_status(self, lock_id):
|
||||
"""Return lock status."""
|
||||
self._update_locks()
|
||||
return self._lock_status_by_id.get(lock_id)
|
||||
|
||||
def get_lock_detail(self, lock_id):
|
||||
"""Return lock detail."""
|
||||
self._update_locks()
|
||||
return self._lock_detail_by_id.get(lock_id)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_locks(self):
|
||||
status_by_id = {}
|
||||
detail_by_id = {}
|
||||
|
||||
for lock in self._locks:
|
||||
status_by_id[lock.device_id] = self._api.get_lock_status(
|
||||
self._access_token, lock.device_id)
|
||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
||||
self._access_token, lock.device_id)
|
||||
|
||||
self._lock_status_by_id = status_by_id
|
||||
self._lock_detail_by_id = detail_by_id
|
||||
|
||||
def lock(self, device_id):
|
||||
"""Lock the device."""
|
||||
return self._api.lock(self._access_token, device_id)
|
||||
|
||||
def unlock(self, device_id):
|
||||
"""Unlock the device."""
|
||||
return self._api.unlock(self._access_token, device_id)
|
||||
@@ -4,7 +4,7 @@ Component to interface with binary sensors.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -28,6 +28,7 @@ DEVICE_CLASSES = [
|
||||
'gas', # On means gas detected, Off means no gas (clear)
|
||||
'heat', # On means hot, Off means normal
|
||||
'light', # On means light detected, Off means no light
|
||||
'lock', # On means open (unlocked), Off means closed (locked)
|
||||
'moisture', # On means wet, Off means dry
|
||||
'motion', # On means motion detected, Off means no motion (clear)
|
||||
'moving', # On means moving, Off means not moving (stopped)
|
||||
@@ -47,13 +48,12 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
97
homeassistant/components/binary_sensor/august.py
Normal file
97
homeassistant/components/binary_sensor/august.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Support for August binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.august/
|
||||
"""
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.components.august import DATA_AUGUST
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
|
||||
DEPENDENCIES = ['august']
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
|
||||
def _retrieve_online_state(data, doorbell):
|
||||
"""Get the latest state of the sensor."""
|
||||
detail = data.get_doorbell_detail(doorbell.device_id)
|
||||
return detail.is_online
|
||||
|
||||
|
||||
def _retrieve_motion_state(data, doorbell):
|
||||
from august.activity import ActivityType
|
||||
return _activity_time_based_state(data, doorbell,
|
||||
[ActivityType.DOORBELL_MOTION,
|
||||
ActivityType.DOORBELL_DING])
|
||||
|
||||
|
||||
def _retrieve_ding_state(data, doorbell):
|
||||
from august.activity import ActivityType
|
||||
return _activity_time_based_state(data, doorbell,
|
||||
[ActivityType.DOORBELL_DING])
|
||||
|
||||
|
||||
def _activity_time_based_state(data, doorbell, activity_types):
|
||||
"""Get the latest state of the sensor."""
|
||||
latest = data.get_latest_device_activity(doorbell.device_id,
|
||||
*activity_types)
|
||||
|
||||
if latest is not None:
|
||||
start = latest.activity_start_time
|
||||
end = latest.activity_end_time + timedelta(seconds=30)
|
||||
return start <= datetime.now() <= end
|
||||
return None
|
||||
|
||||
|
||||
# Sensor types: Name, device_class, state_provider
|
||||
SENSOR_TYPES = {
|
||||
'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state],
|
||||
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
|
||||
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the August binary sensors."""
|
||||
data = hass.data[DATA_AUGUST]
|
||||
devices = []
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
devices.append(AugustBinarySensor(data, sensor_type, doorbell))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class AugustBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an August binary sensor."""
|
||||
|
||||
def __init__(self, data, sensor_type, doorbell):
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self._sensor_type = sensor_type
|
||||
self._doorbell = doorbell
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES[self._sensor_type][1]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return "{} {}".format(self._doorbell.device_name,
|
||||
SENSOR_TYPES[self._sensor_type][0])
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES[self._sensor_type][2]
|
||||
self._state = state_provider(self._data, self._doorbell)
|
||||
@@ -24,7 +24,7 @@ SENSOR_TYPES = {
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES):
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ DEFAULT_NAME = 'Alarm'
|
||||
DEFAULT_PORT = '5007'
|
||||
DEFAULT_SSL = False
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=1)
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(DEVICE_CLASSES),
|
||||
|
||||
@@ -7,7 +7,8 @@ https://home-assistant.io/components/binary_sensor.deconz/
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
|
||||
from homeassistant.components.deconz import (
|
||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
|
||||
@@ -21,7 +22,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
sensors = hass.data[DECONZ_DATA].sensors
|
||||
sensors = hass.data[DATA_DECONZ].sensors
|
||||
entities = []
|
||||
|
||||
for key in sorted(sensors.keys(), key=int):
|
||||
@@ -42,6 +43,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self._sensor.register_async_callback(self.async_update_callback)
|
||||
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, reason):
|
||||
@@ -97,6 +99,6 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
attr = {
|
||||
ATTR_BATTERY_LEVEL: self._sensor.battery,
|
||||
}
|
||||
if self._sensor.type == PRESENCE:
|
||||
if self._sensor.type in PRESENCE:
|
||||
attr['dark'] = self._sensor.dark
|
||||
return attr
|
||||
|
||||
78
homeassistant/components/binary_sensor/egardia.py
Normal file
78
homeassistant/components/binary_sensor/egardia.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Interfaces with Egardia/Woonveilig alarm control panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.egardia/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
|
||||
'Door Contact': 'opening',
|
||||
'IR': 'motion'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Initialize the platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
return
|
||||
|
||||
disc_info = discovery_info[ATTR_DISCOVER_DEVICES]
|
||||
# multiple devices here!
|
||||
async_add_devices(
|
||||
(
|
||||
EgardiaBinarySensor(
|
||||
sensor_id=disc_info[sensor]['id'],
|
||||
name=disc_info[sensor]['name'],
|
||||
egardia_system=hass.data[EGARDIA_DEVICE],
|
||||
device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get(
|
||||
disc_info[sensor]['type'], None)
|
||||
)
|
||||
for sensor in disc_info
|
||||
), True)
|
||||
|
||||
|
||||
class EgardiaBinarySensor(BinarySensorDevice):
|
||||
"""Represents a sensor based on an Egardia sensor (IR, Door Contact)."""
|
||||
|
||||
def __init__(self, sensor_id, name, egardia_system, device_class):
|
||||
"""Initialize the sensor device."""
|
||||
self._id = sensor_id
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._device_class = device_class
|
||||
self._egardia_system = egardia_system
|
||||
|
||||
def update(self):
|
||||
"""Update the status."""
|
||||
egardia_input = self._egardia_system.getsensorstate(self._id)
|
||||
self._state = STATE_ON if egardia_input else STATE_OFF
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Whether the device is switched on."""
|
||||
return self._state == STATE_ON
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""Whether the device is hidden by default."""
|
||||
# these type of sensors are probably mainly used for automations
|
||||
return True
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""The device class."""
|
||||
return self._device_class
|
||||
@@ -50,7 +50,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
self._zone_type = zone_type
|
||||
self._zone_number = zone_number
|
||||
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
_LOGGER.debug('Setting up zone: %s', zone_name)
|
||||
super().__init__(zone_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.4']
|
||||
REQUIREMENTS = ['pyhik==0.1.8']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
@@ -48,6 +48,9 @@ DEVICE_CLASS_MAP = {
|
||||
'Face Detection': 'motion',
|
||||
'Scene Change Detection': 'motion',
|
||||
'I/O': None,
|
||||
'Unattended Baggage': 'motion',
|
||||
'Attended Baggage': 'motion',
|
||||
'Recording Failure': None,
|
||||
}
|
||||
|
||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||
@@ -56,7 +59,7 @@ CUSTOMIZE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
||||
@@ -211,7 +214,7 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
"""Return a unique ID."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
|
||||
@@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.All({
|
||||
vol.Required(CONF_ID): cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_TYPE, default=None): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_INVERTING, default=False): cv.boolean,
|
||||
}, validate_name)
|
||||
])
|
||||
@@ -43,7 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
product_cfg = device['product_cfg']
|
||||
product = device['product']
|
||||
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
|
||||
product_cfg[CONF_TYPE],
|
||||
product_cfg.get(CONF_TYPE),
|
||||
product_cfg[CONF_INVERTING],
|
||||
product)
|
||||
devices.append(sensor)
|
||||
@@ -52,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for sensor_cfg in binary_sensors:
|
||||
ihc_id = sensor_cfg[CONF_ID]
|
||||
name = sensor_cfg[CONF_NAME]
|
||||
sensor_type = sensor_cfg[CONF_TYPE]
|
||||
sensor_type = sensor_cfg.get(CONF_TYPE)
|
||||
inverting = sensor_cfg[CONF_INVERTING]
|
||||
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
|
||||
sensor_type, inverting)
|
||||
@@ -70,7 +70,7 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice):
|
||||
|
||||
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
|
||||
sensor_type: str, inverting: bool,
|
||||
product: Element=None) -> None:
|
||||
product: Element = None) -> None:
|
||||
"""Initialize the IHC binary sensor."""
|
||||
super().__init__(ihc_controller, name, ihc_id, info, product)
|
||||
self._state = None
|
||||
|
||||
@@ -2,86 +2,56 @@
|
||||
Support for INSTEON dimmers via PowerLinc Modem.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/insteon_plm/
|
||||
https://home-assistant.io/components/binary_sensor.insteon_plm/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.insteon_plm import InsteonPLMEntity
|
||||
|
||||
DEPENDENCIES = ['insteon_plm']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
'motionSensor': 'motion',
|
||||
'doorSensor': 'door',
|
||||
'leakSensor': 'moisture'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the INSTEON PLM device class for the hass platform."""
|
||||
plm = hass.data['insteon_plm']
|
||||
|
||||
device_list = []
|
||||
for device in discovery_info:
|
||||
name = device.get('address')
|
||||
address = device.get('address_hex')
|
||||
address = discovery_info['address']
|
||||
device = plm.devices[address]
|
||||
state_key = discovery_info['state_key']
|
||||
|
||||
_LOGGER.info('Registered %s with binary_sensor platform.', name)
|
||||
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
|
||||
device.address.hex, device.states[state_key].name)
|
||||
|
||||
device_list.append(
|
||||
InsteonPLMBinarySensorDevice(hass, plm, address, name)
|
||||
)
|
||||
new_entity = InsteonPLMBinarySensor(device, state_key)
|
||||
|
||||
async_add_devices(device_list)
|
||||
async_add_devices([new_entity])
|
||||
|
||||
|
||||
class InsteonPLMBinarySensorDevice(BinarySensorDevice):
|
||||
"""A Class for an Insteon device."""
|
||||
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
"""A Class for an Insteon device entity."""
|
||||
|
||||
def __init__(self, hass, plm, address, name):
|
||||
"""Initialize the binarysensor."""
|
||||
self._hass = hass
|
||||
self._plm = plm.protocol
|
||||
self._address = address
|
||||
self._name = name
|
||||
|
||||
self._plm.add_update_callback(
|
||||
self.async_binarysensor_update, {'address': self._address})
|
||||
def __init__(self, device, state_key):
|
||||
"""Initialize the INSTEON PLM binary sensor."""
|
||||
super().__init__(device, state_key)
|
||||
self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the address of the node."""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the node."""
|
||||
return self._name
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
sensorstate = self._plm.get_device_attr(self._address, 'sensorstate')
|
||||
_LOGGER.info("Sensor state for %s is %s", self._address, sensorstate)
|
||||
sensorstate = self._insteon_device_state.value
|
||||
return bool(sensorstate)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Provide attributes for display on device card."""
|
||||
insteon_plm = get_component('insteon_plm')
|
||||
return insteon_plm.common_attributes(self)
|
||||
|
||||
def get_attr(self, key):
|
||||
"""Return specified attribute for this device."""
|
||||
return self._plm.get_device_attr(self.address, key)
|
||||
|
||||
@callback
|
||||
def async_binarysensor_update(self, message):
|
||||
"""Receive notification from transport that new data exists."""
|
||||
_LOGGER.info("Received update callback from PLM for %s", self._address)
|
||||
self._hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@@ -56,24 +56,17 @@ def setup_platform(hass, config: ConfigType,
|
||||
else:
|
||||
device_type = _detect_device_type(node)
|
||||
subnode_id = int(node.nid[-1])
|
||||
if device_type == 'opening':
|
||||
# Door/window sensors use an optional "negative" node
|
||||
if subnode_id == 4:
|
||||
if (device_type == 'opening' or device_type == 'moisture'):
|
||||
# These sensors use an optional "negative" subnode 2 to snag
|
||||
# all state changes
|
||||
if subnode_id == 2:
|
||||
parent_device.add_negative_node(node)
|
||||
elif subnode_id == 4:
|
||||
# Subnode 4 is the heartbeat node, which we will represent
|
||||
# as a separate binary_sensor
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
elif subnode_id == 2:
|
||||
parent_device.add_negative_node(node)
|
||||
elif device_type == 'moisture':
|
||||
# Moisture nodes have a subnode 2, but we ignore it because
|
||||
# it's just the inverse of the primary node.
|
||||
if subnode_id == 4:
|
||||
# Heartbeat node
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
else:
|
||||
# We don't yet have any special logic for other sensor types,
|
||||
# so add the nodes as individual devices
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for KNX/IP binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.knx/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -26,6 +25,7 @@ CONF_DEFAULT_HOOK = 'on'
|
||||
CONF_COUNTER = 'counter'
|
||||
CONF_DEFAULT_COUNTER = 1
|
||||
CONF_ACTION = 'action'
|
||||
CONF_RESET_AFTER = 'reset_after'
|
||||
|
||||
CONF__ACTION = 'turn_off_action'
|
||||
|
||||
@@ -35,7 +35,7 @@ DEPENDENCIES = ['knx']
|
||||
AUTOMATION_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string,
|
||||
vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port,
|
||||
vol.Required(CONF_ACTION, default=None): cv.SCRIPT_SCHEMA
|
||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA
|
||||
})
|
||||
|
||||
AUTOMATIONS_SCHEMA = vol.All(
|
||||
@@ -49,16 +49,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
||||
vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_AUTOMATION, default=None): AUTOMATIONS_SCHEMA,
|
||||
vol.Optional(CONF_RESET_AFTER): cv.positive_int,
|
||||
vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up binary sensor(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized:
|
||||
return
|
||||
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
else:
|
||||
@@ -85,7 +83,8 @@ def async_add_devices_config(hass, config, async_add_devices):
|
||||
name=name,
|
||||
group_address=config.get(CONF_ADDRESS),
|
||||
device_class=config.get(CONF_DEVICE_CLASS),
|
||||
significant_bit=config.get(CONF_SIGNIFICANT_BIT))
|
||||
significant_bit=config.get(CONF_SIGNIFICANT_BIT),
|
||||
reset_after=config.get(CONF_RESET_AFTER))
|
||||
hass.data[DATA_KNX].xknx.devices.add(binary_sensor)
|
||||
|
||||
entity = KNXBinarySensor(hass, binary_sensor)
|
||||
@@ -114,11 +113,10 @@ class KNXBinarySensor(BinarySensorDevice):
|
||||
@callback
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
|
||||
@@ -9,7 +9,7 @@ import datetime
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.mercedesme import (
|
||||
DATA_MME, MercedesMeEntity, BINARY_SENSORS)
|
||||
DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS)
|
||||
|
||||
DEPENDENCIES = ['mercedesme']
|
||||
|
||||
@@ -27,8 +27,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices = []
|
||||
for car in data.cars:
|
||||
for key, value in sorted(BINARY_SENSORS.items()):
|
||||
devices.append(MercedesMEBinarySensor(
|
||||
data, key, value[0], car["vin"], None))
|
||||
if car['availabilities'].get(key, 'INVALID') == 'VALID':
|
||||
devices.append(MercedesMEBinarySensor(
|
||||
data, key, value[0], car["vin"], None))
|
||||
else:
|
||||
_LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_PRESENCE_SENSORS, default=PRESENCE_SENSOR_TYPES):
|
||||
vol.Optional(CONF_PRESENCE_SENSORS, default=list(PRESENCE_SENSOR_TYPES)):
|
||||
vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]),
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_WELCOME_SENSORS, default=WELCOME_SENSOR_TYPES):
|
||||
vol.Optional(CONF_WELCOME_SENSORS, default=list(WELCOME_SENSOR_TYPES)):
|
||||
vol.All(cv.ensure_list, [vol.In(WELCOME_SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
@@ -131,8 +131,6 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
self._name += ' / ' + module_name
|
||||
self._sensor_name = sensor
|
||||
self._name += ' ' + sensor
|
||||
self._unique_id = data.camera_data.cameraByName(
|
||||
camera=camera_name, home=home)['id']
|
||||
self._cameratype = camera_type
|
||||
self._state = None
|
||||
|
||||
@@ -141,11 +139,6 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
"""Return the name of the Netatmo device and this sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
|
||||
@@ -27,7 +27,7 @@ SENSOR_TYPES = {
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES):
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
@@ -28,15 +28,15 @@ DEPENDENCIES = ['rfxtrx']
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICES, default={}): {
|
||||
cv.string: vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS, default=None):
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS):
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_OFF_DELAY, default=None):
|
||||
vol.Optional(CONF_OFF_DELAY):
|
||||
vol.Any(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DATA_BITS, default=None): cv.positive_int,
|
||||
vol.Optional(CONF_COMMAND_ON, default=None): cv.byte,
|
||||
vol.Optional(CONF_COMMAND_OFF, default=None): cv.byte
|
||||
vol.Optional(CONF_DATA_BITS): cv.positive_int,
|
||||
vol.Optional(CONF_COMMAND_ON): cv.byte,
|
||||
vol.Optional(CONF_COMMAND_OFF): cv.byte
|
||||
})
|
||||
},
|
||||
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
|
||||
@@ -48,26 +48,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
import RFXtrx as rfxtrxmod
|
||||
sensors = []
|
||||
|
||||
for packet_id, entity in config['devices'].items():
|
||||
for packet_id, entity in config[CONF_DEVICES].items():
|
||||
event = rfxtrx.get_rfx_object(packet_id)
|
||||
device_id = slugify(event.device.id_string.lower())
|
||||
|
||||
if device_id in rfxtrx.RFX_DEVICES:
|
||||
continue
|
||||
|
||||
if entity[CONF_DATA_BITS] is not None:
|
||||
if entity.get(CONF_DATA_BITS) is not None:
|
||||
_LOGGER.debug(
|
||||
"Masked device id: %s", rfxtrx.get_pt2262_deviceid(
|
||||
device_id, entity[CONF_DATA_BITS]))
|
||||
device_id, entity.get(CONF_DATA_BITS)))
|
||||
|
||||
_LOGGER.debug("Add %s rfxtrx.binary_sensor (class %s)",
|
||||
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
|
||||
entity[ATTR_NAME], entity.get(CONF_DEVICE_CLASS))
|
||||
|
||||
device = RfxtrxBinarySensor(
|
||||
event, entity[ATTR_NAME], entity[CONF_DEVICE_CLASS],
|
||||
entity[CONF_FIRE_EVENT], entity[CONF_OFF_DELAY],
|
||||
entity[CONF_DATA_BITS], entity[CONF_COMMAND_ON],
|
||||
entity[CONF_COMMAND_OFF])
|
||||
event, entity.get(CONF_NAME), entity.get(CONF_DEVICE_CLASS),
|
||||
entity[CONF_FIRE_EVENT], entity.get(CONF_OFF_DELAY),
|
||||
entity.get(CONF_DATA_BITS), entity.get(CONF_COMMAND_ON),
|
||||
entity.get(CONF_COMMAND_OFF))
|
||||
device.hass = hass
|
||||
sensors.append(device)
|
||||
rfxtrx.RFX_DEVICES[device_id] = device
|
||||
|
||||
@@ -26,7 +26,7 @@ DEFAULT_SETTLE_TIME = 20
|
||||
DEPENDENCIES = ['rpi_pfio']
|
||||
|
||||
PORT_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_SETTLE_TIME, default=DEFAULT_SETTLE_TIME):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
|
||||
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
binary_sensors = []
|
||||
ports = config.get(CONF_PORTS)
|
||||
for port, port_entity in ports.items():
|
||||
name = port_entity[CONF_NAME]
|
||||
name = port_entity.get(CONF_NAME)
|
||||
settle_time = port_entity[CONF_SETTLE_TIME] / 1000
|
||||
invert_logic = port_entity[CONF_INVERT_LOGIC]
|
||||
|
||||
|
||||
38
homeassistant/components/binary_sensor/upcloud.py
Normal file
38
homeassistant/components/binary_sensor/upcloud.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Support for monitoring the state of UpCloud servers.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.upcloud/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.upcloud import (
|
||||
UpCloudServerEntity, CONF_SERVERS, DATA_UPCLOUD)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['upcloud']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SERVERS): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the UpCloud server binary sensor."""
|
||||
upcloud = hass.data[DATA_UPCLOUD]
|
||||
|
||||
servers = config.get(CONF_SERVERS)
|
||||
|
||||
devices = [UpCloudBinarySensor(upcloud, uuid) for uuid in servers]
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class UpCloudBinarySensor(UpCloudServerEntity, BinarySensorDevice):
|
||||
"""Representation of an UpCloud server sensor."""
|
||||
@@ -47,7 +47,7 @@ DEFAULT_OFFSET = 0
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
|
||||
vol.Optional(CONF_PROVINCE, default=None): cv.string,
|
||||
vol.Optional(CONF_PROVINCE): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
|
||||
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
|
||||
|
||||
@@ -319,7 +319,10 @@ class XiaomiButton(XiaomiBinarySensor):
|
||||
click_type = 'double'
|
||||
elif value == 'both_click':
|
||||
click_type = 'both'
|
||||
elif value == 'shake':
|
||||
click_type = 'shake'
|
||||
else:
|
||||
_LOGGER.warning("Unsupported click_type detected: %s", value)
|
||||
return False
|
||||
|
||||
self._hass.bus.fire('click', {
|
||||
|
||||
@@ -4,7 +4,6 @@ Binary sensors on Zigbee Home Automation networks.
|
||||
For more details on this platform, please refer to the documentation
|
||||
at https://home-assistant.io/components/binary_sensor.zha/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
||||
@@ -25,8 +24,8 @@ CLASS_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Zigbee Home Automation binary sensors."""
|
||||
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||
if discovery_info is None:
|
||||
@@ -39,19 +38,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
device_class = None
|
||||
cluster = in_clusters[IasZone.cluster_id]
|
||||
if discovery_info['new_join']:
|
||||
yield from cluster.bind()
|
||||
await cluster.bind()
|
||||
ieee = cluster.endpoint.device.application.ieee
|
||||
yield from cluster.write_attributes({'cie_addr': ieee})
|
||||
await cluster.write_attributes({'cie_addr': ieee})
|
||||
|
||||
try:
|
||||
zone_type = yield from cluster['zone_type']
|
||||
zone_type = await cluster['zone_type']
|
||||
device_class = CLASS_MAPPING.get(zone_type, None)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# If we fail to read from the device, use a non-specific class
|
||||
pass
|
||||
|
||||
sensor = BinarySensor(device_class, **discovery_info)
|
||||
async_add_devices([sensor])
|
||||
async_add_devices([sensor], update_before_add=True)
|
||||
|
||||
|
||||
class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
@@ -66,6 +65,11 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Let zha handle polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
@@ -83,7 +87,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
if command_id == 0:
|
||||
self._state = args[0] & 3
|
||||
_LOGGER.debug("Updated alarm state: %s", self._state)
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
elif command_id == 1:
|
||||
_LOGGER.debug("Enroll requested")
|
||||
self.hass.add_job(self._ias_zone_cluster.enroll_response(0, 0))
|
||||
res = self._ias_zone_cluster.enroll_response(0, 0)
|
||||
self.hass.async_add_job(res)
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
from bellows.types.basic import uint16_t
|
||||
|
||||
result = await zha.safe_read(self._endpoint.ias_zone,
|
||||
['zone_status'])
|
||||
state = result.get('zone_status', self._state)
|
||||
if isinstance(state, (int, uint16_t)):
|
||||
self._state = result.get('zone_status', self._state) & 3
|
||||
|
||||
105
homeassistant/components/bmw_connected_drive.py
Normal file
105
homeassistant/components/bmw_connected_drive.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Reads vehicle status from BMW connected drive portal.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/bmw_connected_drive/
|
||||
"""
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD
|
||||
)
|
||||
|
||||
REQUIREMENTS = ['bimmer_connected==0.4.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'bmw_connected_drive'
|
||||
CONF_VALUES = 'values'
|
||||
CONF_COUNTRY = 'country'
|
||||
|
||||
ACCOUNT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_COUNTRY): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
cv.string: ACCOUNT_SCHEMA
|
||||
},
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
BMW_COMPONENTS = ['device_tracker', 'sensor']
|
||||
UPDATE_INTERVAL = 5 # in minutes
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the BMW connected drive components."""
|
||||
accounts = []
|
||||
for name, account_config in config[DOMAIN].items():
|
||||
username = account_config[CONF_USERNAME]
|
||||
password = account_config[CONF_PASSWORD]
|
||||
country = account_config[CONF_COUNTRY]
|
||||
_LOGGER.debug('Adding new account %s', name)
|
||||
bimmer = BMWConnectedDriveAccount(username, password, country, name)
|
||||
accounts.append(bimmer)
|
||||
|
||||
# update every UPDATE_INTERVAL minutes, starting now
|
||||
# this should even out the load on the servers
|
||||
|
||||
now = datetime.datetime.now()
|
||||
track_utc_time_change(
|
||||
hass, bimmer.update,
|
||||
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
|
||||
second=now.second)
|
||||
|
||||
hass.data[DOMAIN] = accounts
|
||||
|
||||
for account in accounts:
|
||||
account.update()
|
||||
|
||||
for component in BMW_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BMWConnectedDriveAccount(object):
|
||||
"""Representation of a BMW vehicle."""
|
||||
|
||||
def __init__(self, username: str, password: str, country: str,
|
||||
name: str) -> None:
|
||||
"""Constructor."""
|
||||
from bimmer_connected.account import ConnectedDriveAccount
|
||||
|
||||
self.account = ConnectedDriveAccount(username, password, country)
|
||||
self.name = name
|
||||
self._update_listeners = []
|
||||
|
||||
def update(self, *_):
|
||||
"""Update the state of all vehicles.
|
||||
|
||||
Notify all listeners about the update.
|
||||
"""
|
||||
_LOGGER.debug('Updating vehicle state for account %s, '
|
||||
'notifying %d listeners',
|
||||
self.name, len(self._update_listeners))
|
||||
try:
|
||||
self.account.update_vehicle_states()
|
||||
for listener in self._update_listeners:
|
||||
listener()
|
||||
except IOError as exception:
|
||||
_LOGGER.error('Error updating the vehicle state.')
|
||||
_LOGGER.exception(exception)
|
||||
|
||||
def add_update_listener(self, listener):
|
||||
"""Add a listener for update notifications."""
|
||||
self._update_listeners.append(listener)
|
||||
@@ -166,7 +166,7 @@ class WebDavCalendarData(object):
|
||||
self.event = {
|
||||
"summary": vevent.summary.value,
|
||||
"start": self.get_hass_date(vevent.dtstart.value),
|
||||
"end": self.get_hass_date(vevent.dtend.value),
|
||||
"end": self.get_hass_date(self.get_end_date(vevent)),
|
||||
"location": self.get_attr_value(vevent, "location"),
|
||||
"description": self.get_attr_value(vevent, "description")
|
||||
}
|
||||
@@ -194,7 +194,7 @@ class WebDavCalendarData(object):
|
||||
@staticmethod
|
||||
def is_over(vevent):
|
||||
"""Return if the event is over."""
|
||||
return dt.now() > WebDavCalendarData.to_datetime(vevent.dtend.value)
|
||||
return dt.now() > WebDavCalendarData.get_end_date(vevent)
|
||||
|
||||
@staticmethod
|
||||
def get_hass_date(obj):
|
||||
@@ -217,3 +217,17 @@ class WebDavCalendarData(object):
|
||||
if hasattr(obj, attribute):
|
||||
return getattr(obj, attribute).value
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_end_date(obj):
|
||||
"""Return the end datetime as determined by dtend or duration."""
|
||||
if hasattr(obj, "dtend"):
|
||||
enddate = obj.dtend.value
|
||||
|
||||
elif hasattr(obj, "duration"):
|
||||
enddate = obj.dtstart.value + obj.duration.value
|
||||
|
||||
else:
|
||||
enddate = obj.dtstart.value + timedelta(days=1)
|
||||
|
||||
return WebDavCalendarData.to_datetime(enddate)
|
||||
|
||||
@@ -498,7 +498,7 @@ class TodoistProjectData(object):
|
||||
|
||||
# Organize the best tasks (so users can see all the tasks
|
||||
# they have, organized)
|
||||
while len(project_tasks) > 0:
|
||||
while project_tasks:
|
||||
best_task = self.select_best_task(project_tasks)
|
||||
_LOGGER.debug("Found Todoist Task: %s", best_task[SUMMARY])
|
||||
project_tasks.remove(best_task)
|
||||
|
||||
@@ -264,9 +264,9 @@ class Camera(Entity):
|
||||
'boundary=--frameboundary')
|
||||
yield from response.prepare(request)
|
||||
|
||||
def write(img_bytes):
|
||||
async def write(img_bytes):
|
||||
"""Write image to stream."""
|
||||
response.write(bytes(
|
||||
await response.write(bytes(
|
||||
'--frameboundary\r\n'
|
||||
'Content-Type: {}\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
@@ -282,15 +282,14 @@ class Camera(Entity):
|
||||
break
|
||||
|
||||
if img_bytes and img_bytes != last_image:
|
||||
write(img_bytes)
|
||||
yield from write(img_bytes)
|
||||
|
||||
# Chrome seems to always ignore first picture,
|
||||
# print it twice.
|
||||
if last_image is None:
|
||||
write(img_bytes)
|
||||
yield from write(img_bytes)
|
||||
|
||||
last_image = img_bytes
|
||||
yield from response.drain()
|
||||
|
||||
yield from asyncio.sleep(.5)
|
||||
|
||||
|
||||
76
homeassistant/components/camera/august.py
Normal file
76
homeassistant/components/camera/august.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Support for August camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.august/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.august import DATA_AUGUST, DEFAULT_TIMEOUT
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
DEPENDENCIES = ['august']
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up August cameras."""
|
||||
data = hass.data[DATA_AUGUST]
|
||||
devices = []
|
||||
|
||||
for doorbell in data.doorbells:
|
||||
devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class AugustCamera(Camera):
|
||||
"""An implementation of a Canary security camera."""
|
||||
|
||||
def __init__(self, data, doorbell, timeout):
|
||||
"""Initialize a Canary security camera."""
|
||||
super().__init__()
|
||||
self._data = data
|
||||
self._doorbell = doorbell
|
||||
self._timeout = timeout
|
||||
self._image_url = None
|
||||
self._image_content = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._doorbell.device_name
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return self._doorbell.has_subscription
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the camera brand."""
|
||||
return 'August'
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
return 'Doorbell'
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
latest = self._data.get_doorbell_detail(self._doorbell.device_id)
|
||||
|
||||
if self._image_url is not latest.image_url:
|
||||
self._image_url = latest.image_url
|
||||
self._image_content = requests.get(self._image_url,
|
||||
timeout=self._timeout).content
|
||||
|
||||
return self._image_content
|
||||
@@ -18,8 +18,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
DEPENDENCIES = ['doorbird']
|
||||
|
||||
_CAMERA_LAST_VISITOR = "DoorBird Last Ring"
|
||||
_CAMERA_LAST_MOTION = "DoorBird Last Motion"
|
||||
_CAMERA_LIVE = "DoorBird Live"
|
||||
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
|
||||
_LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1)
|
||||
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_TIMEOUT = 10 # seconds
|
||||
@@ -34,6 +36,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
DoorBirdCamera(
|
||||
device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR,
|
||||
_LAST_VISITOR_INTERVAL),
|
||||
DoorBirdCamera(
|
||||
device.history_image_url(1, 'motionsensor'), _CAMERA_LAST_MOTION,
|
||||
_LAST_MOTION_INTERVAL),
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,8 @@ class MjpegCamera(Camera):
|
||||
else:
|
||||
req = requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||
|
||||
# https://github.com/PyCQA/pylint/issues/1437
|
||||
# pylint: disable=no-member
|
||||
with closing(req) as response:
|
||||
return extract_image_from_mjpeg(response.iter_content(102400))
|
||||
|
||||
|
||||
@@ -67,8 +67,6 @@ class NetatmoCamera(Camera):
|
||||
self._vpnurl, self._localurl = self._data.camera_data.cameraUrls(
|
||||
camera=camera_name
|
||||
)
|
||||
self._unique_id = data.camera_data.cameraByName(
|
||||
camera=camera_name, home=home)['id']
|
||||
self._cameratype = camera_type
|
||||
|
||||
def camera_image(self):
|
||||
@@ -112,8 +110,3 @@ class NetatmoCamera(Camera):
|
||||
elif self._cameratype == "NACamera":
|
||||
return "Welcome"
|
||||
return None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this camera."""
|
||||
return self._unique_id
|
||||
|
||||
@@ -6,18 +6,19 @@ https://home-assistant.io/components/camera.onvif/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, DOMAIN
|
||||
from homeassistant.components.ffmpeg import (
|
||||
DATA_FFMPEG, CONF_EXTRA_ARGUMENTS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream)
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,6 +33,25 @@ DEFAULT_PORT = 5000
|
||||
DEFAULT_USERNAME = 'admin'
|
||||
DEFAULT_PASSWORD = '888888'
|
||||
DEFAULT_ARGUMENTS = '-q:v 2'
|
||||
DEFAULT_PROFILE = 0
|
||||
|
||||
CONF_PROFILE = "profile"
|
||||
|
||||
ATTR_PAN = "pan"
|
||||
ATTR_TILT = "tilt"
|
||||
ATTR_ZOOM = "zoom"
|
||||
|
||||
DIR_UP = "UP"
|
||||
DIR_DOWN = "DOWN"
|
||||
DIR_LEFT = "LEFT"
|
||||
DIR_RIGHT = "RIGHT"
|
||||
ZOOM_OUT = "ZOOM_OUT"
|
||||
ZOOM_IN = "ZOOM_IN"
|
||||
|
||||
SERVICE_PTZ = "onvif_ptz"
|
||||
|
||||
ONVIF_DATA = "onvif"
|
||||
ENTITIES = "entities"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@@ -40,38 +60,108 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
})
|
||||
|
||||
SERVICE_PTZ_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
ATTR_PAN: vol.In([DIR_LEFT, DIR_RIGHT]),
|
||||
ATTR_TILT: vol.In([DIR_UP, DIR_DOWN]),
|
||||
ATTR_ZOOM: vol.In([ZOOM_OUT, ZOOM_IN])
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a ONVIF camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)):
|
||||
return
|
||||
async_add_devices([ONVIFCamera(hass, config)])
|
||||
|
||||
def handle_ptz(service):
|
||||
"""Handle PTZ service call."""
|
||||
pan = service.data.get(ATTR_PAN, None)
|
||||
tilt = service.data.get(ATTR_TILT, None)
|
||||
zoom = service.data.get(ATTR_ZOOM, None)
|
||||
all_cameras = hass.data[ONVIF_DATA][ENTITIES]
|
||||
entity_ids = extract_entity_ids(hass, service)
|
||||
target_cameras = []
|
||||
if not entity_ids:
|
||||
target_cameras = all_cameras
|
||||
else:
|
||||
target_cameras = [camera for camera in all_cameras
|
||||
if camera.entity_id in entity_ids]
|
||||
for camera in target_cameras:
|
||||
camera.perform_ptz(pan, tilt, zoom)
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_PTZ, handle_ptz,
|
||||
schema=SERVICE_PTZ_SCHEMA)
|
||||
add_devices([ONVIFHassCamera(hass, config)])
|
||||
|
||||
|
||||
class ONVIFCamera(Camera):
|
||||
class ONVIFHassCamera(Camera):
|
||||
"""An implementation of an ONVIF camera."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a ONVIF camera."""
|
||||
from onvif import ONVIFService
|
||||
import onvif
|
||||
from onvif import ONVIFCamera, exceptions
|
||||
super().__init__()
|
||||
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
media = ONVIFService(
|
||||
'http://{}:{}/onvif/device_service'.format(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT)),
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
'{}/wsdl/media.wsdl'.format(os.path.dirname(onvif.__file__))
|
||||
)
|
||||
self._input = media.GetStreamUri().Uri
|
||||
_LOGGER.debug("ONVIF Camera Using the following URL for %s: %s",
|
||||
self._name, self._input)
|
||||
self._input = None
|
||||
camera = None
|
||||
try:
|
||||
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
|
||||
config.get(CONF_HOST), config.get(CONF_PORT))
|
||||
camera = ONVIFCamera(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)
|
||||
)
|
||||
media_service = camera.create_media_service()
|
||||
self._profiles = media_service.GetProfiles()
|
||||
self._profile_index = config.get(CONF_PROFILE)
|
||||
if self._profile_index >= len(self._profiles):
|
||||
_LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d."
|
||||
" Using the last profile.",
|
||||
self._name, self._profile_index)
|
||||
self._profile_index = -1
|
||||
req = media_service.create_type('GetStreamUri')
|
||||
# pylint: disable=protected-access
|
||||
req.ProfileToken = self._profiles[self._profile_index]._token
|
||||
self._input = media_service.GetStreamUri(req).Uri.replace(
|
||||
'rtsp://', 'rtsp://{}:{}@'.format(
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD)), 1)
|
||||
_LOGGER.debug(
|
||||
"ONVIF Camera Using the following URL for %s: %s",
|
||||
self._name, self._input)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Unable to communicate with ONVIF Camera: %s", err)
|
||||
raise
|
||||
try:
|
||||
self._ptz = camera.create_ptz_service()
|
||||
except exceptions.ONVIFError as err:
|
||||
self._ptz = None
|
||||
_LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err)
|
||||
|
||||
def perform_ptz(self, pan, tilt, zoom):
|
||||
"""Perform a PTZ action on the camera."""
|
||||
if self._ptz:
|
||||
pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0
|
||||
tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0
|
||||
zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
|
||||
req = {"Velocity": {
|
||||
"PanTilt": {"_x": pan_val, "_y": tilt_val},
|
||||
"Zoom": {"_x": zoom_val}}}
|
||||
self._ptz.ContinuousMove(req)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
if ONVIF_DATA not in self.hass.data:
|
||||
self.hass.data[ONVIF_DATA] = {}
|
||||
self.hass.data[ONVIF_DATA][ENTITIES] = []
|
||||
self.hass.data[ONVIF_DATA][ENTITIES].append(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
|
||||
262
homeassistant/components/camera/proxy.py
Normal file
262
homeassistant/components/camera/proxy.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Proxy camera platform that enables image processing of camera data.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/proxy
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ENTITY_ID, HTTP_HEADER_HA_AUTH)
|
||||
from homeassistant.components.camera import (
|
||||
PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web)
|
||||
|
||||
REQUIREMENTS = ['pillow==5.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_MAX_IMAGE_WIDTH = "max_image_width"
|
||||
CONF_IMAGE_QUALITY = "image_quality"
|
||||
CONF_IMAGE_REFRESH_RATE = "image_refresh_rate"
|
||||
CONF_FORCE_RESIZE = "force_resize"
|
||||
CONF_MAX_STREAM_WIDTH = "max_stream_width"
|
||||
CONF_STREAM_QUALITY = "stream_quality"
|
||||
CONF_CACHE_IMAGES = "cache_images"
|
||||
|
||||
DEFAULT_BASENAME = "Camera Proxy"
|
||||
DEFAULT_QUALITY = 75
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
|
||||
vol.Optional(CONF_IMAGE_QUALITY): int,
|
||||
vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
|
||||
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
|
||||
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
|
||||
vol.Optional(CONF_MAX_STREAM_WIDTH): int,
|
||||
vol.Optional(CONF_STREAM_QUALITY): int,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Proxy camera platform."""
|
||||
async_add_devices([ProxyCamera(hass, config)])
|
||||
|
||||
|
||||
async def _read_frame(req):
|
||||
"""Read a single frame from an MJPEG stream."""
|
||||
# based on https://gist.github.com/russss/1143799
|
||||
import cgi
|
||||
# Read in HTTP headers:
|
||||
stream = req.content
|
||||
# multipart/x-mixed-replace; boundary=--frameboundary
|
||||
_mimetype, options = cgi.parse_header(req.headers['content-type'])
|
||||
boundary = options.get('boundary').encode('utf-8')
|
||||
if not boundary:
|
||||
_LOGGER.error("Malformed MJPEG missing boundary")
|
||||
raise Exception("Can't find content-type")
|
||||
|
||||
line = await stream.readline()
|
||||
# Seek ahead to the first chunk
|
||||
while line.strip() != boundary:
|
||||
line = await stream.readline()
|
||||
# Read in chunk headers
|
||||
while line.strip() != b'':
|
||||
parts = line.split(b':')
|
||||
if len(parts) > 1 and parts[0].lower() == b'content-length':
|
||||
# Grab chunk length
|
||||
length = int(parts[1].strip())
|
||||
line = await stream.readline()
|
||||
image = await stream.read(length)
|
||||
return image
|
||||
|
||||
|
||||
def _resize_image(image, opts):
|
||||
"""Resize image."""
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
if not opts:
|
||||
return image
|
||||
|
||||
quality = opts.quality or DEFAULT_QUALITY
|
||||
new_width = opts.max_width
|
||||
|
||||
img = Image.open(io.BytesIO(image))
|
||||
imgfmt = str(img.format)
|
||||
if imgfmt != 'PNG' and imgfmt != 'JPEG':
|
||||
_LOGGER.debug("Image is of unsupported type: %s", imgfmt)
|
||||
return image
|
||||
|
||||
(old_width, old_height) = img.size
|
||||
old_size = len(image)
|
||||
if old_width <= new_width:
|
||||
if opts.quality is None:
|
||||
_LOGGER.debug("Image is smaller-than / equal-to requested width")
|
||||
return image
|
||||
new_width = old_width
|
||||
|
||||
scale = new_width / float(old_width)
|
||||
new_height = int((float(old_height)*float(scale)))
|
||||
|
||||
img = img.resize((new_width, new_height), Image.ANTIALIAS)
|
||||
imgbuf = io.BytesIO()
|
||||
img.save(imgbuf, "JPEG", optimize=True, quality=quality)
|
||||
newimage = imgbuf.getvalue()
|
||||
if not opts.force_resize and len(newimage) >= old_size:
|
||||
_LOGGER.debug("Using original image(%d bytes) "
|
||||
"because resized image (%d bytes) is not smaller",
|
||||
old_size, len(newimage))
|
||||
return image
|
||||
|
||||
_LOGGER.debug("Resized image "
|
||||
"from (%dx%d - %d bytes) "
|
||||
"to (%dx%d - %d bytes)",
|
||||
old_width, old_height, old_size,
|
||||
new_width, new_height, len(newimage))
|
||||
return newimage
|
||||
|
||||
|
||||
class ImageOpts():
|
||||
"""The representation of image options."""
|
||||
|
||||
def __init__(self, max_width, quality, force_resize):
|
||||
"""Initialize image options."""
|
||||
self.max_width = max_width
|
||||
self.quality = quality
|
||||
self.force_resize = force_resize
|
||||
|
||||
def __bool__(self):
|
||||
"""Bool evalution rules."""
|
||||
return bool(self.max_width or self.quality)
|
||||
|
||||
|
||||
class ProxyCamera(Camera):
|
||||
"""The representation of a Proxy camera."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a proxy camera component."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._proxied_camera = config.get(CONF_ENTITY_ID)
|
||||
self._name = (
|
||||
config.get(CONF_NAME) or
|
||||
"{} - {}".format(DEFAULT_BASENAME, self._proxied_camera))
|
||||
self._image_opts = ImageOpts(
|
||||
config.get(CONF_MAX_IMAGE_WIDTH),
|
||||
config.get(CONF_IMAGE_QUALITY),
|
||||
config.get(CONF_FORCE_RESIZE))
|
||||
|
||||
self._stream_opts = ImageOpts(
|
||||
config.get(CONF_MAX_STREAM_WIDTH),
|
||||
config.get(CONF_STREAM_QUALITY),
|
||||
True)
|
||||
|
||||
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
|
||||
self._cache_images = bool(
|
||||
config.get(CONF_IMAGE_REFRESH_RATE)
|
||||
or config.get(CONF_CACHE_IMAGES))
|
||||
self._last_image_time = 0
|
||||
self._last_image = None
|
||||
self._headers = (
|
||||
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
|
||||
if self.hass.config.api.api_password is not None
|
||||
else None)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
now = dt_util.utcnow()
|
||||
|
||||
if (self._image_refresh_rate and
|
||||
now < self._last_image_time + self._image_refresh_rate):
|
||||
return self._last_image
|
||||
|
||||
self._last_image_time = now
|
||||
url = "{}/api/camera_proxy/{}".format(
|
||||
self.hass.config.api.base_url, self._proxied_camera)
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = await websession.get(url, headers=self._headers)
|
||||
image = await response.read()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout getting camera image")
|
||||
return self._last_image
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Error getting new camera image: %s", err)
|
||||
return self._last_image
|
||||
|
||||
image = await self.hass.async_add_job(
|
||||
_resize_image, image, self._image_opts)
|
||||
|
||||
if self._cache_images:
|
||||
self._last_image = image
|
||||
return image
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
websession = async_get_clientsession(self.hass)
|
||||
url = "{}/api/camera_proxy_stream/{}".format(
|
||||
self.hass.config.api.base_url, self._proxied_camera)
|
||||
stream_coro = websession.get(url, headers=self._headers)
|
||||
|
||||
if not self._stream_opts:
|
||||
await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
return
|
||||
|
||||
response = aiohttp.web.StreamResponse()
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--frameboundary')
|
||||
await response.prepare(request)
|
||||
|
||||
def write(img_bytes):
|
||||
"""Write image to stream."""
|
||||
response.write(bytes(
|
||||
'--frameboundary\r\n'
|
||||
'Content-Type: {}\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
self.content_type, len(img_bytes)),
|
||||
'utf-8') + img_bytes + b'\r\n')
|
||||
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
req = await stream_coro
|
||||
|
||||
try:
|
||||
while True:
|
||||
image = await _read_frame(req)
|
||||
if not image:
|
||||
break
|
||||
image = await self.hass.async_add_job(
|
||||
_resize_image, image, self._stream_opts)
|
||||
write(image)
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
req.close()
|
||||
response = None
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
await response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
@@ -8,6 +8,7 @@ import os
|
||||
import subprocess
|
||||
import logging
|
||||
import shutil
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -36,7 +37,7 @@ DEFAULT_TIMELAPSE = 1000
|
||||
DEFAULT_VERTICAL_FLIP = 0
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FILE_PATH): cv.string,
|
||||
vol.Optional(CONF_FILE_PATH): cv.isfile,
|
||||
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
||||
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT):
|
||||
@@ -77,27 +78,36 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
CONF_TIMELAPSE: config.get(CONF_TIMELAPSE),
|
||||
CONF_HORIZONTAL_FLIP: config.get(CONF_HORIZONTAL_FLIP),
|
||||
CONF_VERTICAL_FLIP: config.get(CONF_VERTICAL_FLIP),
|
||||
CONF_FILE_PATH: config.get(CONF_FILE_PATH,
|
||||
os.path.join(os.path.dirname(__file__),
|
||||
'image.jpg'))
|
||||
CONF_FILE_PATH: config.get(CONF_FILE_PATH)
|
||||
}
|
||||
)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill)
|
||||
|
||||
try:
|
||||
# Try to create an empty file (or open existing) to ensure we have
|
||||
# proper permissions.
|
||||
open(setup_config[CONF_FILE_PATH], 'a').close()
|
||||
file_path = setup_config[CONF_FILE_PATH]
|
||||
|
||||
add_devices([RaspberryCamera(setup_config)])
|
||||
except PermissionError:
|
||||
_LOGGER.error("File path is not writable")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
_LOGGER.error("Could not create output file (missing directory?)")
|
||||
def delete_temp_file(*args):
|
||||
"""Delete the temporary file to prevent saving multiple temp images.
|
||||
|
||||
Only used when no path is defined
|
||||
"""
|
||||
os.remove(file_path)
|
||||
|
||||
# If no file path is defined, use a temporary file
|
||||
if file_path is None:
|
||||
temp_file = NamedTemporaryFile(suffix='.jpg', delete=False)
|
||||
temp_file.close()
|
||||
file_path = temp_file.name
|
||||
setup_config[CONF_FILE_PATH] = file_path
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, delete_temp_file)
|
||||
|
||||
# Check whether the file path has been whitelisted
|
||||
elif not hass.config.is_allowed_path(file_path):
|
||||
_LOGGER.error("'%s' is not a whitelisted directory", file_path)
|
||||
return False
|
||||
|
||||
add_devices([RaspberryCamera(setup_config)])
|
||||
|
||||
|
||||
class RaspberryCamera(Camera):
|
||||
"""Representation of a Raspberry Pi camera."""
|
||||
|
||||
@@ -23,3 +23,20 @@ snapshot:
|
||||
filename:
|
||||
description: Template of a Filename. Variable is entity_id.
|
||||
example: '/tmp/snapshot_{{ entity_id }}'
|
||||
|
||||
onvif_ptz:
|
||||
description: Pan/Tilt/Zoom service for ONVIF camera.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to pan, tilt or zoom.
|
||||
example: 'camera.living_room_camera'
|
||||
pan:
|
||||
description: "Direction of pan. Allowed values: LEFT, RIGHT."
|
||||
example: 'LEFT'
|
||||
tilt:
|
||||
description: "Direction of tilt. Allowed values: DOWN, UP."
|
||||
example: 'DOWN'
|
||||
zoom:
|
||||
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
|
||||
example: "ZOOM_IN"
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ class UnifiVideoCamera(Camera):
|
||||
self._nvr.set_recordmode(self._uuid, set_mode)
|
||||
self._motion_status = mode
|
||||
except NvrError as err:
|
||||
_LOGGER.error("Unable to set recordmode to " + set_mode)
|
||||
_LOGGER.error("Unable to set recordmode to %s", set_mode)
|
||||
_LOGGER.debug(err)
|
||||
|
||||
def enable_motion_detection(self):
|
||||
|
||||
@@ -33,7 +33,7 @@ CAMERAS_SCHEMA = vol.Schema({
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_CAMERAS, default={}):
|
||||
vol.Optional(CONF_CAMERAS):
|
||||
vol.Schema(vol.All(cv.ensure_list, [CAMERAS_SCHEMA])),
|
||||
vol.Optional(CONF_NEW_VERSION, default=True): cv.boolean,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
@@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Discover and setup Xeoma Cameras."""
|
||||
from pyxeoma.xeoma import Xeoma, XeomaError
|
||||
@@ -68,7 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
for image_name, username, pw in discovered_image_names
|
||||
]
|
||||
|
||||
for cam in config[CONF_CAMERAS]:
|
||||
for cam in config.get(CONF_CAMERAS, []):
|
||||
# https://github.com/PyCQA/pylint/issues/1830
|
||||
# pylint: disable=stop-iteration-return
|
||||
camera = next(
|
||||
(dc for dc in discovered_cameras
|
||||
if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None)
|
||||
|
||||
@@ -38,8 +38,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass,
|
||||
config,
|
||||
async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up a Yi Camera."""
|
||||
_LOGGER.debug('Received configuration: %s', config)
|
||||
async_add_devices([YiCamera(hass, config)], True)
|
||||
@@ -107,31 +109,29 @@ class YiCamera(Camera):
|
||||
self.user, self.passwd, self.host, self.port, self.path,
|
||||
latest_dir, videos[-1])
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
|
||||
url = yield from self.hass.async_add_job(self.get_latest_video_url)
|
||||
url = await self.hass.async_add_job(self.get_latest_video_url)
|
||||
if url != self._last_url:
|
||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
||||
self._last_image = yield from asyncio.shield(ffmpeg.get_image(
|
||||
self._last_image = await asyncio.shield(ffmpeg.get_image(
|
||||
url, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
||||
self._last_url = url
|
||||
|
||||
return self._last_image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
await stream.open_camera(
|
||||
self._last_url, extra_cmd=self._extra_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
await stream.close()
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['py-canary==0.4.0']
|
||||
REQUIREMENTS = ['py-canary==0.4.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -237,14 +237,12 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up climate devices."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
yield from component.async_setup(config)
|
||||
await component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_away_mode_set_service(service):
|
||||
async def async_away_mode_set_service(service):
|
||||
"""Set away mode on target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -253,23 +251,22 @@ def async_setup(hass, config):
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if away_mode:
|
||||
yield from climate.async_turn_away_mode_on()
|
||||
await climate.async_turn_away_mode_on()
|
||||
else:
|
||||
yield from climate.async_turn_away_mode_off()
|
||||
await climate.async_turn_away_mode_off()
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
|
||||
schema=SET_AWAY_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_hold_mode_set_service(service):
|
||||
async def async_hold_mode_set_service(service):
|
||||
"""Set hold mode on target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -277,21 +274,20 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_hold_mode(hold_mode)
|
||||
await climate.async_set_hold_mode(hold_mode)
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
|
||||
schema=SET_HOLD_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_aux_heat_set_service(service):
|
||||
async def async_aux_heat_set_service(service):
|
||||
"""Set auxiliary heater on target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -300,23 +296,22 @@ def async_setup(hass, config):
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if aux_heat:
|
||||
yield from climate.async_turn_aux_heat_on()
|
||||
await climate.async_turn_aux_heat_on()
|
||||
else:
|
||||
yield from climate.async_turn_aux_heat_off()
|
||||
await climate.async_turn_aux_heat_off()
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
|
||||
schema=SET_AUX_HEAT_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_temperature_set_service(service):
|
||||
async def async_temperature_set_service(service):
|
||||
"""Set temperature on the target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -333,21 +328,20 @@ def async_setup(hass, config):
|
||||
else:
|
||||
kwargs[value] = temp
|
||||
|
||||
yield from climate.async_set_temperature(**kwargs)
|
||||
await climate.async_set_temperature(**kwargs)
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
|
||||
schema=SET_TEMPERATURE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_humidity_set_service(service):
|
||||
async def async_humidity_set_service(service):
|
||||
"""Set humidity on the target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -355,20 +349,19 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_humidity(humidity)
|
||||
await climate.async_set_humidity(humidity)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
|
||||
schema=SET_HUMIDITY_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_fan_mode_set_service(service):
|
||||
async def async_fan_mode_set_service(service):
|
||||
"""Set fan mode on target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -376,20 +369,19 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_fan_mode(fan)
|
||||
await climate.async_set_fan_mode(fan)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
|
||||
schema=SET_FAN_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_operation_set_service(service):
|
||||
async def async_operation_set_service(service):
|
||||
"""Set operating mode on the target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -397,20 +389,19 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_operation_mode(operation_mode)
|
||||
await climate.async_set_operation_mode(operation_mode)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
|
||||
schema=SET_OPERATION_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_swing_set_service(service):
|
||||
async def async_swing_set_service(service):
|
||||
"""Set swing mode on the target climate devices."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
@@ -418,36 +409,35 @@ def async_setup(hass, config):
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_swing_mode(swing_mode)
|
||||
await climate.async_set_swing_mode(swing_mode)
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
|
||||
schema=SET_SWING_MODE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_on_off_service(service):
|
||||
async def async_on_off_service(service):
|
||||
"""Handle on/off calls."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
yield from climate.async_turn_on()
|
||||
await climate.async_turn_on()
|
||||
elif service.service == SERVICE_TURN_OFF:
|
||||
yield from climate.async_turn_off()
|
||||
await climate.async_turn_off()
|
||||
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
update_tasks.append(climate.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_on_off_service,
|
||||
@@ -669,16 +659,16 @@ class ClimateDevice(Entity):
|
||||
"""
|
||||
return self.hass.async_add_job(self.set_humidity, humidity)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_fan_mode(self, fan):
|
||||
def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.set_fan_mode, fan)
|
||||
return self.hass.async_add_job(self.set_fan_mode, fan_mode)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
|
||||
@@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
HA_STATE_TO_DAIKIN = {
|
||||
@@ -236,9 +236,9 @@ class DaikinClimate(ClimateDevice):
|
||||
"""Return the fan setting."""
|
||||
return self.get(ATTR_FAN_MODE)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set fan mode."""
|
||||
self.set({ATTR_FAN_MODE: fan})
|
||||
self.set({ATTR_FAN_MODE: fan_mode})
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
||||
@@ -195,9 +195,9 @@ class DemoClimate(ClimateDevice):
|
||||
self._current_swing_mode = swing_mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target temperature."""
|
||||
self._current_fan_mode = fan
|
||||
self._current_fan_mode = fan_mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
@@ -225,9 +225,9 @@ class DemoClimate(ClimateDevice):
|
||||
self._away = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_hold_mode(self, hold):
|
||||
"""Update hold mode on."""
|
||||
self._hold = hold
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Update hold_mode on."""
|
||||
self._hold = hold_mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
|
||||
@@ -98,8 +98,7 @@ class EphEmberThermostat(ClimateDevice):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self._zone['isCurrentlyActive']:
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return STATE_IDLE
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.8']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
# pylint: disable=import-error
|
||||
# pylint: disable=import-error, no-name-in-module
|
||||
class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Representation of an eQ-3 Bluetooth Smart thermostat."""
|
||||
|
||||
@@ -75,6 +75,8 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
self._name = _name
|
||||
self._thermostat = eq3.Thermostat(_mac)
|
||||
self._target_temperature = None
|
||||
self._target_mode = None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@@ -116,6 +118,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
self._target_temperature = temperature
|
||||
self._thermostat.target_temperature = temperature
|
||||
|
||||
@property
|
||||
@@ -132,6 +135,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
self._target_mode = operation_mode
|
||||
self._thermostat.mode = self.reverse_modes[operation_mode]
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
@@ -177,3 +181,15 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
self._thermostat.update()
|
||||
except BTLEException as ex:
|
||||
_LOGGER.warning("Updating the state failed: %s", ex)
|
||||
|
||||
if (self._target_temperature and
|
||||
self._thermostat.target_temperature
|
||||
!= self._target_temperature):
|
||||
self.set_temperature(temperature=self._target_temperature)
|
||||
else:
|
||||
self._target_temperature = None
|
||||
if (self._target_mode and
|
||||
self.modes[self._thermostat.mode] != self._target_mode):
|
||||
self.set_operation_mode(operation_mode=self._target_mode)
|
||||
else:
|
||||
self._target_mode = None
|
||||
|
||||
@@ -152,6 +152,6 @@ class Flexit(ClimateDevice):
|
||||
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.unit.set_temp(self._target_temperature)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new fan mode."""
|
||||
self.unit.set_fan_speed(self._fan_list.index(fan))
|
||||
self.unit.set_fan_speed(self._fan_list.index(fan_mode))
|
||||
|
||||
@@ -190,11 +190,9 @@ class GenericThermostat(ClimateDevice):
|
||||
"""Return the current state."""
|
||||
if self._is_device_active:
|
||||
return self.current_operation
|
||||
else:
|
||||
if self._enabled:
|
||||
return STATE_IDLE
|
||||
else:
|
||||
return STATE_OFF
|
||||
if self._enabled:
|
||||
return STATE_IDLE
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for KNX/IP climate devices.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.knx/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -48,9 +47,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
default=DEFAULT_SETPOINT_SHIFT_STEP): vol.All(
|
||||
float, vol.Range(min=0, max=2)),
|
||||
vol.Optional(CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX):
|
||||
vol.All(int, vol.Range(min=-32, max=0)),
|
||||
vol.Optional(CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN):
|
||||
vol.All(int, vol.Range(min=0, max=32)),
|
||||
vol.Optional(CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN):
|
||||
vol.All(int, vol.Range(min=-32, max=0)),
|
||||
vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
|
||||
@@ -61,12 +60,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up climate(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized:
|
||||
return
|
||||
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
else:
|
||||
@@ -138,11 +134,10 @@ class KNXClimate(ClimateDevice):
|
||||
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
@@ -190,14 +185,13 @@ class KNXClimate(ClimateDevice):
|
||||
"""Return the maximum temperature."""
|
||||
return self.device.target_temperature_max
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
yield from self.device.set_target_temperature(temperature)
|
||||
yield from self.async_update_ha_state()
|
||||
await self.device.set_target_temperature(temperature)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
@@ -213,10 +207,9 @@ class KNXClimate(ClimateDevice):
|
||||
operation_mode in
|
||||
self.device.get_supported_operation_modes()]
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_operation_mode(self, operation_mode):
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if self.device.supports_operation_mode:
|
||||
from xknx.knx import HVACOperationMode
|
||||
knx_operation_mode = HVACOperationMode(operation_mode)
|
||||
yield from self.device.set_operation_mode(knx_operation_mode)
|
||||
await self.device.set_operation_mode(knx_operation_mode)
|
||||
|
||||
@@ -26,7 +26,7 @@ SUPPORT_FLAGS = (SUPPORT_FAN_MODE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_ON_OFF | SUPPORT_TARGET_TEMPERATURE)
|
||||
|
||||
OP_MODES = [
|
||||
STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT
|
||||
STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT
|
||||
]
|
||||
|
||||
FAN_MODES = [
|
||||
@@ -42,8 +42,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
all_devices = []
|
||||
|
||||
for device in devices:
|
||||
all_devices.append(MelissaClimate(
|
||||
api, device['serial_number'], device))
|
||||
if device['type'] == 'melissa':
|
||||
all_devices.append(MelissaClimate(
|
||||
api, device['serial_number'], device))
|
||||
|
||||
add_devices(all_devices)
|
||||
|
||||
@@ -146,10 +147,10 @@ class MelissaClimate(ClimateDevice):
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.send({self._api.TEMP: temp})
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set fan mode."""
|
||||
fan_mode = self.hass_fan_to_melissa(fan)
|
||||
self.send({self._api.FAN: fan_mode})
|
||||
melissa_fan_mode = self.hass_fan_to_melissa(fan_mode)
|
||||
self.send({self._api.FAN: melissa_fan_mode})
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
@@ -174,8 +175,7 @@ class MelissaClimate(ClimateDevice):
|
||||
if not self._api.send(self._serial_number, self._cur_settings):
|
||||
self._cur_settings = old_value
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Get latest data from Melissa."""
|
||||
@@ -196,14 +196,11 @@ class MelissaClimate(ClimateDevice):
|
||||
return STATE_OFF
|
||||
elif state == self._api.STATE_IDLE:
|
||||
return STATE_IDLE
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def melissa_op_to_hass(self, mode):
|
||||
"""Translate Melissa modes to hass states."""
|
||||
if mode == self._api.MODE_AUTO:
|
||||
return STATE_AUTO
|
||||
elif mode == self._api.MODE_HEAT:
|
||||
if mode == self._api.MODE_HEAT:
|
||||
return STATE_HEAT
|
||||
elif mode == self._api.MODE_COOL:
|
||||
return STATE_COOL
|
||||
@@ -211,10 +208,9 @@ class MelissaClimate(ClimateDevice):
|
||||
return STATE_DRY
|
||||
elif mode == self._api.MODE_FAN:
|
||||
return STATE_FAN_ONLY
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Operation mode %s could not be mapped to hass", mode)
|
||||
return None
|
||||
_LOGGER.warning(
|
||||
"Operation mode %s could not be mapped to hass", mode)
|
||||
return None
|
||||
|
||||
def melissa_fan_to_hass(self, fan):
|
||||
"""Translate Melissa fan modes to hass modes."""
|
||||
@@ -226,15 +222,12 @@ class MelissaClimate(ClimateDevice):
|
||||
return SPEED_MEDIUM
|
||||
elif fan == self._api.FAN_HIGH:
|
||||
return SPEED_HIGH
|
||||
else:
|
||||
_LOGGER.warning("Fan mode %s could not be mapped to hass", fan)
|
||||
return None
|
||||
_LOGGER.warning("Fan mode %s could not be mapped to hass", fan)
|
||||
return None
|
||||
|
||||
def hass_mode_to_melissa(self, mode):
|
||||
"""Translate hass states to melissa modes."""
|
||||
if mode == STATE_AUTO:
|
||||
return self._api.MODE_AUTO
|
||||
elif mode == STATE_HEAT:
|
||||
if mode == STATE_HEAT:
|
||||
return self._api.MODE_HEAT
|
||||
elif mode == STATE_COOL:
|
||||
return self._api.MODE_COOL
|
||||
|
||||
@@ -482,15 +482,15 @@ class MqttClimate(MqttAvailability, ClimateDevice):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_fan_mode(self, fan):
|
||||
def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target temperature."""
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_FAN_MODE_COMMAND_TOPIC],
|
||||
fan, self._qos, self._retain)
|
||||
fan_mode, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
|
||||
self._current_fan_mode = fan
|
||||
self._current_fan_mode = fan_mode
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -552,15 +552,15 @@ class MqttClimate(MqttAvailability, ClimateDevice):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_hold_mode(self, hold):
|
||||
def async_set_hold_mode(self, hold_mode):
|
||||
"""Update hold mode on."""
|
||||
if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_HOLD_COMMAND_TOPIC],
|
||||
hold, self._qos, self._retain)
|
||||
hold_mode, self._qos, self._retain)
|
||||
|
||||
if self._topic[CONF_HOLD_STATE_TOPIC] is None:
|
||||
self._hold = hold
|
||||
self._hold = hold_mode
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -143,14 +143,14 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
self._values[value_type] = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target temperature."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan)
|
||||
self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode)
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
self._values[set_req.V_HVAC_SPEED] = fan
|
||||
self._values[set_req.V_HVAC_SPEED] = fan_mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
|
||||
@@ -29,10 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
NEST_MODE_HEAT_COOL = 'heat-cool'
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Nest thermostat."""
|
||||
@@ -58,6 +54,10 @@ class NestThermostat(ClimateDevice):
|
||||
self.device = device
|
||||
self._fan_list = [STATE_ON, STATE_AUTO]
|
||||
|
||||
# Set the default supported features
|
||||
self._support_flags = (SUPPORT_TARGET_TEMPERATURE |
|
||||
SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE)
|
||||
|
||||
# Not all nest devices support cooling and heating remove unused
|
||||
self._operation_list = [STATE_OFF]
|
||||
|
||||
@@ -70,11 +70,16 @@ class NestThermostat(ClimateDevice):
|
||||
|
||||
if self.device.can_heat and self.device.can_cool:
|
||||
self._operation_list.append(STATE_AUTO)
|
||||
self._support_flags = (self._support_flags |
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
|
||||
self._operation_list.append(STATE_ECO)
|
||||
|
||||
# feature of device
|
||||
self._has_fan = self.device.has_fan
|
||||
if self._has_fan:
|
||||
self._support_flags = (self._support_flags | SUPPORT_FAN_MODE)
|
||||
|
||||
# data attributes
|
||||
self._away = None
|
||||
@@ -95,7 +100,7 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -162,6 +167,7 @@ class NestThermostat(ClimateDevice):
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
import nest
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
@@ -170,7 +176,10 @@ class NestThermostat(ClimateDevice):
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
self.device.target = temp
|
||||
try:
|
||||
self.device.target = temp
|
||||
except nest.nest.APIError:
|
||||
_LOGGER.error("An error occured while setting the temperature")
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
@@ -205,11 +214,14 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return self._fan_list
|
||||
if self._has_fan:
|
||||
return self._fan_list
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Turn fan on/off."""
|
||||
self.device.fan = fan.lower()
|
||||
if self._has_fan:
|
||||
self.device.fan = fan_mode.lower()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
@@ -225,7 +237,7 @@ class NestThermostat(ClimateDevice):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._humidity = self.device.humidity,
|
||||
self._humidity = self.device.humidity
|
||||
self._temperature = self.device.temperature
|
||||
self._mode = self.device.mode
|
||||
self._target_temperature = self.device.target
|
||||
|
||||
@@ -185,7 +185,7 @@ class NuHeatThermostat(ClimateDevice):
|
||||
self._thermostat.resume_schedule()
|
||||
self._force_update = True
|
||||
|
||||
def set_hold_mode(self, hold_mode, **kwargs):
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Update the hold mode of the thermostat."""
|
||||
if hold_mode == MODE_AUTO:
|
||||
schedule_mode = SCHEDULE_RUN
|
||||
|
||||
@@ -183,17 +183,16 @@ class RadioThermostat(ClimateDevice):
|
||||
"""List of available fan modes."""
|
||||
if self._is_model_ct80:
|
||||
return CT80_FAN_OPERATION_LIST
|
||||
else:
|
||||
return CT30_FAN_OPERATION_LIST
|
||||
return CT30_FAN_OPERATION_LIST
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
return self._fmode
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Turn fan on/off."""
|
||||
code = FAN_MODE_TO_CODE.get(fan, None)
|
||||
code = FAN_MODE_TO_CODE.get(fan_mode, None)
|
||||
if code is not None:
|
||||
self.device.fmode = code
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ REQUIREMENTS = ['pysensibo==1.0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ALL = 'all'
|
||||
ALL = ['all']
|
||||
TIMEOUT = 10
|
||||
|
||||
SERVICE_ASSUME_STATE = 'sensibo_assume_state'
|
||||
@@ -240,13 +240,13 @@ class SensiboClimate(ClimateDevice):
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._temperatures_list[0] \
|
||||
if len(self._temperatures_list) else super().min_temp()
|
||||
if self._temperatures_list else super().min_temp()
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._temperatures_list[-1] \
|
||||
if len(self._temperatures_list) else super().max_temp()
|
||||
if self._temperatures_list else super().max_temp()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
@@ -273,11 +273,11 @@ class SensiboClimate(ClimateDevice):
|
||||
self._id, 'targetTemperature', temperature, self._ac_states)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_fan_mode(self, fan):
|
||||
def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
with async_timeout.timeout(TIMEOUT):
|
||||
yield from self._client.async_set_ac_state_property(
|
||||
self._id, 'fanLevel', fan, self._ac_states)
|
||||
self._id, 'fanLevel', fan_mode, self._ac_states)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_operation_mode(self, operation_mode):
|
||||
|
||||
@@ -213,6 +213,7 @@ class TadoClimate(ClimateDevice):
|
||||
self._target_temp = temperature
|
||||
self._control_heating()
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def set_operation_mode(self, readable_operation_mode):
|
||||
"""Set new operation mode."""
|
||||
operation_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
@@ -51,8 +51,7 @@ class TeslaThermostat(TeslaDevice, ClimateDevice):
|
||||
mode = self.tesla_device.is_hvac_enabled()
|
||||
if mode:
|
||||
return OPERATION_LIST[0] # On
|
||||
else:
|
||||
return OPERATION_LIST[1] # Off
|
||||
return OPERATION_LIST[1] # Off
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
|
||||
@@ -111,8 +111,7 @@ class VenstarThermostat(ClimateDevice):
|
||||
"""Return the unit of measurement, as defined by the API."""
|
||||
if self._client.tempunits == self._client.TEMPUNITS_F:
|
||||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
return TEMP_CELSIUS
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
@@ -143,16 +142,14 @@ class VenstarThermostat(ClimateDevice):
|
||||
return STATE_COOL
|
||||
elif self._client.mode == self._client.MODE_AUTO:
|
||||
return STATE_AUTO
|
||||
else:
|
||||
return STATE_OFF
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
if self._client.fan == self._client.FAN_AUTO:
|
||||
return STATE_AUTO
|
||||
else:
|
||||
return STATE_ON
|
||||
return STATE_ON
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -169,24 +166,21 @@ class VenstarThermostat(ClimateDevice):
|
||||
return self._client.heattemp
|
||||
elif self._client.mode == self._client.MODE_COOL:
|
||||
return self._client.cooltemp
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temp if auto mode is on."""
|
||||
if self._client.mode == self._client.MODE_AUTO:
|
||||
return self._client.heattemp
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temp if auto mode is on."""
|
||||
if self._client.mode == self._client.MODE_AUTO:
|
||||
return self._client.cooltemp
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_humidity(self):
|
||||
@@ -245,9 +239,9 @@ class VenstarThermostat(ClimateDevice):
|
||||
if not success:
|
||||
_LOGGER.error("Failed to change the temperature")
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
if fan == STATE_ON:
|
||||
if fan_mode == STATE_ON:
|
||||
success = self._client.set_fan(self._client.FAN_ON)
|
||||
else:
|
||||
success = self._client.set_fan(self._client.FAN_AUTO)
|
||||
|
||||
@@ -85,13 +85,13 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
"""Return a list of available fan modes."""
|
||||
return FAN_OPERATION_LIST
|
||||
|
||||
def set_fan_mode(self, mode):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target temperature."""
|
||||
if mode == FAN_OPERATION_LIST[0]:
|
||||
if fan_mode == FAN_OPERATION_LIST[0]:
|
||||
self.vera_device.fan_on()
|
||||
elif mode == FAN_OPERATION_LIST[1]:
|
||||
elif fan_mode == FAN_OPERATION_LIST[1]:
|
||||
self.vera_device.fan_auto()
|
||||
elif mode == FAN_OPERATION_LIST[2]:
|
||||
elif fan_mode == FAN_OPERATION_LIST[2]:
|
||||
return self.vera_device.fan_cycle()
|
||||
|
||||
@property
|
||||
|
||||
@@ -324,9 +324,9 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return self.wink.fan_modes()
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Turn fan on/off."""
|
||||
self.wink.set_fan_mode(fan.lower())
|
||||
self.wink.set_fan_mode(fan_mode.lower())
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxiliary heater on."""
|
||||
@@ -486,26 +486,25 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
return SPEED_LOW
|
||||
elif speed <= 0.66:
|
||||
return SPEED_MEDIUM
|
||||
else:
|
||||
return SPEED_HIGH
|
||||
return SPEED_HIGH
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return a list of available fan modes."""
|
||||
return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""
|
||||
Set fan speed.
|
||||
|
||||
The official Wink app only supports 3 modes [low, medium, high]
|
||||
which are equal to [0.33, 0.66, 1.0] respectively.
|
||||
"""
|
||||
if fan == SPEED_LOW:
|
||||
if fan_mode == SPEED_LOW:
|
||||
speed = 0.33
|
||||
elif fan == SPEED_MEDIUM:
|
||||
elif fan_mode == SPEED_MEDIUM:
|
||||
speed = 0.66
|
||||
elif fan == SPEED_HIGH:
|
||||
elif fan_mode == SPEED_HIGH:
|
||||
speed = 1.0
|
||||
self.wink.set_ac_fan_speed(speed)
|
||||
|
||||
|
||||
@@ -198,10 +198,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
|
||||
self.values.primary.data = temperature
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set new target fan mode."""
|
||||
if self.values.fan_mode:
|
||||
self.values.fan_mode.data = fan
|
||||
self.values.fan_mode.data = fan_mode
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
|
||||
@@ -15,12 +15,12 @@ import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE)
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME)
|
||||
from homeassistant.helpers import entityfilter, config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.alexa import smart_home as alexa_sh
|
||||
from homeassistant.components.google_assistant import smart_home as ga_sh
|
||||
from homeassistant.components.google_assistant import helpers as ga_h
|
||||
|
||||
from . import http_api, iot
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
@@ -51,15 +51,11 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
|
||||
GOOGLE_ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_TYPE): vol.In(ga_sh.MAPPING_COMPONENT),
|
||||
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
|
||||
})
|
||||
|
||||
ASSISTANT_SCHEMA = vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_FILTER,
|
||||
default=lambda: entityfilter.generate_filter([], [], [], [])
|
||||
): entityfilter.FILTER_SCHEMA,
|
||||
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
|
||||
})
|
||||
|
||||
ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
|
||||
@@ -178,7 +174,7 @@ class Cloud:
|
||||
"""If an entity should be exposed."""
|
||||
return conf['filter'](entity.entity_id)
|
||||
|
||||
self._gactions_config = ga_sh.Config(
|
||||
self._gactions_config = ga_h.Config(
|
||||
should_expose=should_expose,
|
||||
agent_user_id=self.claims['cognito:username'],
|
||||
entity_config=conf.get(CONF_ENTITY_CONFIG),
|
||||
@@ -222,7 +218,7 @@ class Cloud:
|
||||
|
||||
# Fetching keyset can fail if internet is not up yet.
|
||||
if not success:
|
||||
self.hass.helpers.async_call_later(5, self.async_start)
|
||||
self.hass.helpers.event.async_call_later(5, self.async_start)
|
||||
return
|
||||
|
||||
def load_config():
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
"""Package to communicate with the authentication API."""
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudError(Exception):
|
||||
@@ -20,17 +17,11 @@ class UserNotConfirmed(CloudError):
|
||||
"""Raised when a user has not confirmed email yet."""
|
||||
|
||||
|
||||
class ExpiredCode(CloudError):
|
||||
"""Raised when an expired code is encountered."""
|
||||
|
||||
|
||||
class InvalidCode(CloudError):
|
||||
"""Raised when an invalid code is submitted."""
|
||||
|
||||
|
||||
class PasswordChangeRequired(CloudError):
|
||||
"""Raised when a password change is required."""
|
||||
|
||||
# https://github.com/PyCQA/pylint/issues/1085
|
||||
# pylint: disable=useless-super-delegation
|
||||
def __init__(self, message='Password change required.'):
|
||||
"""Initialize a password change required error."""
|
||||
super().__init__(message)
|
||||
@@ -43,10 +34,8 @@ class UnknownError(CloudError):
|
||||
AWS_EXCEPTIONS = {
|
||||
'UserNotFoundException': UserNotFound,
|
||||
'NotAuthorizedException': Unauthenticated,
|
||||
'ExpiredCodeException': ExpiredCode,
|
||||
'UserNotConfirmedException': UserNotConfirmed,
|
||||
'PasswordResetRequiredException': PasswordChangeRequired,
|
||||
'CodeMismatchException': InvalidCode,
|
||||
}
|
||||
|
||||
|
||||
@@ -70,17 +59,6 @@ def register(cloud, email, password):
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def confirm_register(cloud, confirmation_code, email):
|
||||
"""Confirm confirmation code after registration."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(cloud)
|
||||
try:
|
||||
cognito.confirm_sign_up(confirmation_code, email)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def resend_email_confirm(cloud, email):
|
||||
"""Resend email confirmation."""
|
||||
from botocore.exceptions import ClientError
|
||||
@@ -108,18 +86,6 @@ def forgot_password(cloud, email):
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def confirm_forgot_password(cloud, confirmation_code, email, new_password):
|
||||
"""Confirm forgotten password code and change password."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(cloud, username=email)
|
||||
|
||||
try:
|
||||
cognito.confirm_forgot_password(confirmation_code, new_password)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def login(cloud, email, password):
|
||||
"""Log user in and fetch certificate."""
|
||||
cognito = _authenticate(cloud, email, password)
|
||||
|
||||
@@ -16,3 +16,9 @@ MESSAGE_EXPIRATION = """
|
||||
It looks like your Home Assistant Cloud subscription has expired. Please check
|
||||
your [account page](/config/cloud/account) to continue using the service.
|
||||
"""
|
||||
|
||||
MESSAGE_AUTH_FAIL = """
|
||||
You have been logged out of Home Assistant Cloud because we have been unable
|
||||
to verify your credentials. Please [log in](/config/cloud) again to continue
|
||||
using the service.
|
||||
"""
|
||||
|
||||
@@ -6,8 +6,9 @@ import logging
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import (
|
||||
HomeAssistantView, RequestDataValidator)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import (
|
||||
RequestDataValidator)
|
||||
|
||||
from . import auth_api
|
||||
from .const import DOMAIN, REQUEST_TIMEOUT
|
||||
@@ -22,10 +23,8 @@ def async_setup(hass):
|
||||
hass.http.register_view(CloudLogoutView)
|
||||
hass.http.register_view(CloudAccountView)
|
||||
hass.http.register_view(CloudRegisterView)
|
||||
hass.http.register_view(CloudConfirmRegisterView)
|
||||
hass.http.register_view(CloudResendConfirmView)
|
||||
hass.http.register_view(CloudForgotPasswordView)
|
||||
hass.http.register_view(CloudConfirmForgotPasswordView)
|
||||
|
||||
|
||||
_CLOUD_ERRORS = {
|
||||
@@ -33,8 +32,6 @@ _CLOUD_ERRORS = {
|
||||
auth_api.UserNotConfirmed: (400, 'Email not confirmed.'),
|
||||
auth_api.Unauthenticated: (401, 'Authentication failed.'),
|
||||
auth_api.PasswordChangeRequired: (400, 'Password change required.'),
|
||||
auth_api.ExpiredCode: (400, 'Confirmation code has expired.'),
|
||||
auth_api.InvalidCode: (400, 'Invalid confirmation code.'),
|
||||
asyncio.TimeoutError: (502, 'Unable to reach the Home Assistant cloud.')
|
||||
}
|
||||
|
||||
@@ -148,31 +145,6 @@ class CloudRegisterView(HomeAssistantView):
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudConfirmRegisterView(HomeAssistantView):
|
||||
"""Confirm registration on the Home Assistant cloud."""
|
||||
|
||||
url = '/api/cloud/confirm_register'
|
||||
name = 'api:cloud:confirm_register'
|
||||
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('confirmation_code'): str,
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle registration confirmation request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_register, cloud, data['confirmation_code'],
|
||||
data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudResendConfirmView(HomeAssistantView):
|
||||
"""Resend email confirmation code."""
|
||||
|
||||
@@ -219,33 +191,6 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
class CloudConfirmForgotPasswordView(HomeAssistantView):
|
||||
"""View to finish Forgot Password flow.."""
|
||||
|
||||
url = '/api/cloud/confirm_forgot_password'
|
||||
name = 'api:cloud:confirm_forgot_password'
|
||||
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('confirmation_code'): str,
|
||||
vol.Required('email'): str,
|
||||
vol.Required('new_password'): vol.All(str, vol.Length(min=6))
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password confirm request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_forgot_password, cloud,
|
||||
data['confirmation_code'], data['email'],
|
||||
data['new_password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
def _account_data(cloud):
|
||||
"""Generate the auth data JSON response."""
|
||||
claims = cloud.claims
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Module to handle messages from Home Assistant cloud."""
|
||||
import asyncio
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
from aiohttp import hdrs, client_exceptions, WSMsgType
|
||||
|
||||
@@ -10,7 +11,7 @@ from homeassistant.components.google_assistant import smart_home as ga
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from . import auth_api
|
||||
from .const import MESSAGE_EXPIRATION
|
||||
from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL
|
||||
|
||||
HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -44,20 +45,13 @@ class CloudIoT:
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
"""Connect to the IoT broker."""
|
||||
if self.state != STATE_DISCONNECTED:
|
||||
raise RuntimeError('Connect called while not disconnected')
|
||||
|
||||
hass = self.cloud.hass
|
||||
if self.cloud.subscription_expired:
|
||||
# Try refreshing the token to see if it is still expired.
|
||||
yield from hass.async_add_job(auth_api.check_token, self.cloud)
|
||||
|
||||
if self.cloud.subscription_expired:
|
||||
hass.components.persistent_notification.async_create(
|
||||
MESSAGE_EXPIRATION, 'Subscription expired',
|
||||
'cloud_subscription_expired')
|
||||
self.state = STATE_DISCONNECTED
|
||||
return
|
||||
|
||||
if self.state == STATE_CONNECTED:
|
||||
raise RuntimeError('Already connected')
|
||||
self.close_requested = False
|
||||
self.state = STATE_CONNECTING
|
||||
self.tries = 0
|
||||
|
||||
@asyncio.coroutine
|
||||
def _handle_hass_stop(event):
|
||||
@@ -66,17 +60,70 @@ class CloudIoT:
|
||||
remove_hass_stop_listener = None
|
||||
yield from self.disconnect()
|
||||
|
||||
self.state = STATE_CONNECTING
|
||||
self.close_requested = False
|
||||
remove_hass_stop_listener = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
|
||||
|
||||
while True:
|
||||
try:
|
||||
yield from self._handle_connection()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Safety net. This should never hit.
|
||||
# Still adding it here to make sure we can always reconnect
|
||||
_LOGGER.exception("Unexpected error")
|
||||
|
||||
if self.close_requested:
|
||||
break
|
||||
|
||||
self.state = STATE_CONNECTING
|
||||
self.tries += 1
|
||||
|
||||
try:
|
||||
# Sleep 2^tries seconds between retries
|
||||
self.retry_task = hass.async_add_job(asyncio.sleep(
|
||||
2**min(9, self.tries), loop=hass.loop))
|
||||
yield from self.retry_task
|
||||
self.retry_task = None
|
||||
except asyncio.CancelledError:
|
||||
# Happens if disconnect called
|
||||
break
|
||||
|
||||
self.state = STATE_DISCONNECTED
|
||||
if remove_hass_stop_listener is not None:
|
||||
remove_hass_stop_listener()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _handle_connection(self):
|
||||
"""Connect to the IoT broker."""
|
||||
hass = self.cloud.hass
|
||||
|
||||
try:
|
||||
yield from hass.async_add_job(auth_api.check_token, self.cloud)
|
||||
except auth_api.Unauthenticated as err:
|
||||
_LOGGER.error('Unable to refresh token: %s', err)
|
||||
|
||||
hass.components.persistent_notification.async_create(
|
||||
MESSAGE_AUTH_FAIL, 'Home Assistant Cloud',
|
||||
'cloud_subscription_expired')
|
||||
|
||||
# Don't await it because it will cancel this task
|
||||
hass.async_add_job(self.cloud.logout())
|
||||
return
|
||||
except auth_api.CloudError as err:
|
||||
_LOGGER.warning("Unable to refresh token: %s", err)
|
||||
return
|
||||
|
||||
if self.cloud.subscription_expired:
|
||||
hass.components.persistent_notification.async_create(
|
||||
MESSAGE_EXPIRATION, 'Home Assistant Cloud',
|
||||
'cloud_subscription_expired')
|
||||
self.close_requested = True
|
||||
return
|
||||
|
||||
session = async_get_clientsession(self.cloud.hass)
|
||||
client = None
|
||||
disconnect_warn = None
|
||||
|
||||
try:
|
||||
yield from hass.async_add_job(auth_api.check_token, self.cloud)
|
||||
|
||||
self.client = client = yield from session.ws_connect(
|
||||
self.cloud.relayer, heartbeat=55, headers={
|
||||
hdrs.AUTHORIZATION:
|
||||
@@ -90,9 +137,11 @@ class CloudIoT:
|
||||
while not client.closed:
|
||||
msg = yield from client.receive()
|
||||
|
||||
if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED,
|
||||
WSMsgType.CLOSING):
|
||||
disconnect_warn = 'Connection cancelled.'
|
||||
if msg.type in (WSMsgType.CLOSED, WSMsgType.CLOSING):
|
||||
break
|
||||
|
||||
elif msg.type == WSMsgType.ERROR:
|
||||
disconnect_warn = 'Connection error'
|
||||
break
|
||||
|
||||
elif msg.type != WSMsgType.TEXT:
|
||||
@@ -106,7 +155,9 @@ class CloudIoT:
|
||||
disconnect_warn = 'Received invalid JSON.'
|
||||
break
|
||||
|
||||
_LOGGER.debug("Received message: %s", msg)
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_LOGGER.debug("Received message:\n%s\n",
|
||||
pprint.pformat(msg))
|
||||
|
||||
response = {
|
||||
'msgid': msg['msgid'],
|
||||
@@ -128,12 +179,11 @@ class CloudIoT:
|
||||
_LOGGER.exception("Error handling message")
|
||||
response['error'] = 'exception'
|
||||
|
||||
_LOGGER.debug("Publishing message: %s", response)
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_LOGGER.debug("Publishing message:\n%s\n",
|
||||
pprint.pformat(response))
|
||||
yield from client.send_json(response)
|
||||
|
||||
except auth_api.CloudError:
|
||||
_LOGGER.warning("Unable to connect: Unable to refresh token.")
|
||||
|
||||
except client_exceptions.WSServerHandshakeError as err:
|
||||
if err.code == 401:
|
||||
disconnect_warn = 'Invalid auth.'
|
||||
@@ -145,38 +195,11 @@ class CloudIoT:
|
||||
except client_exceptions.ClientError as err:
|
||||
_LOGGER.warning("Unable to connect: %s", err)
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
if not self.close_requested:
|
||||
_LOGGER.exception("Unexpected error")
|
||||
|
||||
finally:
|
||||
if disconnect_warn is not None:
|
||||
_LOGGER.warning("Connection closed: %s", disconnect_warn)
|
||||
|
||||
if remove_hass_stop_listener is not None:
|
||||
remove_hass_stop_listener()
|
||||
|
||||
if client is not None:
|
||||
self.client = None
|
||||
yield from client.close()
|
||||
|
||||
if self.close_requested:
|
||||
self.state = STATE_DISCONNECTED
|
||||
|
||||
if disconnect_warn is None:
|
||||
_LOGGER.info("Connection closed")
|
||||
else:
|
||||
self.state = STATE_CONNECTING
|
||||
self.tries += 1
|
||||
|
||||
try:
|
||||
# Sleep 0, 5, 10, 15 ... up to 30 seconds between retries
|
||||
self.retry_task = hass.async_add_job(asyncio.sleep(
|
||||
min(30, (self.tries - 1) * 5), loop=hass.loop))
|
||||
yield from self.retry_task
|
||||
self.retry_task = None
|
||||
hass.async_add_job(self.connect())
|
||||
except asyncio.CancelledError:
|
||||
# Happens if disconnect called
|
||||
pass
|
||||
_LOGGER.warning("Connection closed: %s", disconnect_warn)
|
||||
|
||||
@asyncio.coroutine
|
||||
def disconnect(self):
|
||||
|
||||
@@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['coinbase==2.0.7']
|
||||
REQUIREMENTS = ['coinbase==2.1.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -13,16 +13,25 @@ from homeassistant.util.yaml import load_yaml, dump
|
||||
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script')
|
||||
ON_DEMAND = ('zwave')
|
||||
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
|
||||
'entity_registry')
|
||||
ON_DEMAND = ('zwave',)
|
||||
FEATURE_FLAGS = ('config_entries',)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the config component."""
|
||||
global SECTIONS
|
||||
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
'config', 'config', 'mdi:settings')
|
||||
|
||||
# Temporary way of allowing people to opt-in for unreleased config sections
|
||||
for key, value in config.get(DOMAIN, {}).items():
|
||||
if key in FEATURE_FLAGS and value:
|
||||
SECTIONS += (key,)
|
||||
|
||||
@asyncio.coroutine
|
||||
def setup_panel(panel_name):
|
||||
"""Set up a panel."""
|
||||
@@ -151,7 +160,7 @@ class EditKeyBasedConfigView(BaseEditConfigView):
|
||||
|
||||
def _get_value(self, hass, data, config_key):
|
||||
"""Get value."""
|
||||
return data.get(config_key, {})
|
||||
return data.get(config_key)
|
||||
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
|
||||
182
homeassistant/components/config/config_entries.py
Normal file
182
homeassistant/components/config/config_entries.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Http views to control the config manager."""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
|
||||
|
||||
REQUIREMENTS = ['voluptuous-serialize==1']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
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(ConfigManagerAvailableFlowView)
|
||||
return True
|
||||
|
||||
|
||||
def _prepare_json(result):
|
||||
"""Convert result for JSON."""
|
||||
if result['type'] != config_entries.RESULT_TYPE_FORM:
|
||||
return result
|
||||
|
||||
import voluptuous_serialize
|
||||
|
||||
data = result.copy()
|
||||
|
||||
schema = data['data_schema']
|
||||
if schema is None:
|
||||
data['data_schema'] = []
|
||||
else:
|
||||
data['data_schema'] = voluptuous_serialize.convert(schema)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ConfigManagerEntryIndexView(HomeAssistantView):
|
||||
"""View to get available config entries."""
|
||||
|
||||
url = '/api/config/config_entries/entry'
|
||||
name = 'api:config:config_entries:entry'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""List flows in progress."""
|
||||
hass = request.app['hass']
|
||||
return self.json([{
|
||||
'entry_id': entry.entry_id,
|
||||
'domain': entry.domain,
|
||||
'title': entry.title,
|
||||
'source': entry.source,
|
||||
'state': entry.state,
|
||||
} for entry in hass.config_entries.async_entries()])
|
||||
|
||||
|
||||
class ConfigManagerEntryResourceView(HomeAssistantView):
|
||||
"""View to interact with a config entry."""
|
||||
|
||||
url = '/api/config/config_entries/entry/{entry_id}'
|
||||
name = 'api:config:config_entries:entry:resource'
|
||||
|
||||
@asyncio.coroutine
|
||||
def delete(self, request, entry_id):
|
||||
"""Delete a config entry."""
|
||||
hass = request.app['hass']
|
||||
|
||||
try:
|
||||
result = yield from hass.config_entries.async_remove(entry_id)
|
||||
except config_entries.UnknownEntry:
|
||||
return self.json_message('Invalid entry specified', 404)
|
||||
|
||||
return self.json(result)
|
||||
|
||||
|
||||
class ConfigManagerFlowIndexView(HomeAssistantView):
|
||||
"""View to create config flows."""
|
||||
|
||||
url = '/api/config/config_entries/flow'
|
||||
name = 'api:config:config_entries:flow'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""List flows that are in progress but not started by a user.
|
||||
|
||||
Example of a non-user initiated flow is a discovered Hue hub that
|
||||
requires user interaction to finish setup.
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
class ConfigManagerFlowResourceView(HomeAssistantView):
|
||||
"""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."""
|
||||
|
||||
url = '/api/config/config_entries/flow_handlers'
|
||||
name = 'api:config:config_entries:flow_handlers'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""List available flow handlers."""
|
||||
return self.json(config_entries.FLOWS)
|
||||
55
homeassistant/components/config/entity_registry.py
Normal file
55
homeassistant/components/config/entity_registry.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""HTTP views to interact with the entity registry."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.helpers.entity_registry import async_get_registry
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Enable the Entity Registry views."""
|
||||
hass.http.register_view(ConfigManagerEntityView)
|
||||
return True
|
||||
|
||||
|
||||
class ConfigManagerEntityView(HomeAssistantView):
|
||||
"""View to interact with an entity registry entry."""
|
||||
|
||||
url = '/api/config/entity_registry/{entity_id}'
|
||||
name = 'api:config:entity_registry:entity'
|
||||
|
||||
async def get(self, request, entity_id):
|
||||
"""Get the entity registry settings for an entity."""
|
||||
hass = request.app['hass']
|
||||
registry = await async_get_registry(hass)
|
||||
entry = registry.entities.get(entity_id)
|
||||
|
||||
if entry is None:
|
||||
return self.json_message('Entry not found', 404)
|
||||
|
||||
return self.json(_entry_dict(entry))
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
# If passed in, we update value. Passing None will remove old value.
|
||||
vol.Optional('name'): vol.Any(str, None),
|
||||
}))
|
||||
async def post(self, request, entity_id, data):
|
||||
"""Update the entity registry settings for an entity."""
|
||||
hass = request.app['hass']
|
||||
registry = await async_get_registry(hass)
|
||||
|
||||
if entity_id not in registry.entities:
|
||||
return self.json_message('Entry not found', 404)
|
||||
|
||||
entry = registry.async_update_entity(entity_id, **data)
|
||||
return self.json(_entry_dict(entry))
|
||||
|
||||
|
||||
@callback
|
||||
def _entry_dict(entry):
|
||||
"""Helper to convert entry to API format."""
|
||||
return {
|
||||
'entity_id': entry.entity_id,
|
||||
'name': entry.name
|
||||
}
|
||||
102
homeassistant/components/config_entry_example.py
Normal file
102
homeassistant/components/config_entry_example.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Example component to show how config entries work."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.util import slugify
|
||||
|
||||
|
||||
DOMAIN = 'config_entry_example'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup for our example component."""
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_entry(hass, entry):
|
||||
"""Initialize an entry."""
|
||||
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
|
||||
hass.states.async_set(entity_id, 'loaded', {
|
||||
ATTR_FRIENDLY_NAME: entry.data['name']
|
||||
})
|
||||
|
||||
# Indicate setup was successful.
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_unload_entry(hass, entry):
|
||||
"""Unload an entry."""
|
||||
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
|
||||
hass.states.async_remove(entity_id)
|
||||
|
||||
# Indicate unload was successful.
|
||||
return True
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class ExampleConfigFlow(config_entries.ConfigFlowHandler):
|
||||
"""Handle an example configuration flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a Hue config handler."""
|
||||
self.object_id = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_step_init(self, user_input=None):
|
||||
"""Start config flow."""
|
||||
errors = None
|
||||
if user_input is not None:
|
||||
object_id = user_input['object_id']
|
||||
|
||||
if object_id != '' and object_id == slugify(object_id):
|
||||
self.object_id = user_input['object_id']
|
||||
return (yield from self.async_step_name())
|
||||
|
||||
errors = {
|
||||
'object_id': 'Invalid object id.'
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
title='Pick object id',
|
||||
step_id='init',
|
||||
description="Please enter an object_id for the test entity.",
|
||||
data_schema=vol.Schema({
|
||||
'object_id': str
|
||||
}),
|
||||
errors=errors
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_step_name(self, user_input=None):
|
||||
"""Ask user to enter the name."""
|
||||
errors = None
|
||||
if user_input is not None:
|
||||
name = user_input['name']
|
||||
|
||||
if name != '':
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={
|
||||
'name': name,
|
||||
'object_id': self.object_id,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
title='Name of the entity',
|
||||
step_id='name',
|
||||
description="Please enter a name for the test entity.",
|
||||
data_schema=vol.Schema({
|
||||
'name': str
|
||||
}),
|
||||
errors=errors
|
||||
)
|
||||
@@ -4,22 +4,19 @@ Support for functionality to have conversations with Home Assistant.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/conversation/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components import http
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
from homeassistant.components.http.data_validator import (
|
||||
RequestDataValidator)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.16.0']
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,9 +25,6 @@ ATTR_TEXT = 'text'
|
||||
DEPENDENCIES = ['http']
|
||||
DOMAIN = 'conversation'
|
||||
|
||||
INTENT_TURN_OFF = 'HassTurnOff'
|
||||
INTENT_TURN_ON = 'HassTurnOn'
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
REGEX_TYPE = type(re.compile(''))
|
||||
|
||||
@@ -50,7 +44,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
|
||||
@core.callback
|
||||
@bind_hass
|
||||
def async_register(hass, intent_type, utterances):
|
||||
"""Register an intent.
|
||||
"""Register utterances and any custom intents.
|
||||
|
||||
Registrations don't require conversations to be loaded. They will become
|
||||
active once the conversation component is loaded.
|
||||
@@ -72,11 +66,8 @@ def async_register(hass, intent_type, utterances):
|
||||
conf.append(_create_matcher(utterance))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Register the process service."""
|
||||
warnings.filterwarnings('ignore', module='fuzzywuzzy')
|
||||
|
||||
config = config.get(DOMAIN, {})
|
||||
intents = hass.data.get(DOMAIN)
|
||||
|
||||
@@ -91,49 +82,73 @@ def async_setup(hass, config):
|
||||
|
||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
||||
|
||||
@asyncio.coroutine
|
||||
def process(service):
|
||||
async def process(service):
|
||||
"""Parse text into commands."""
|
||||
text = service.data[ATTR_TEXT]
|
||||
yield from _process(hass, text)
|
||||
try:
|
||||
await _process(hass, text)
|
||||
except intent.IntentHandleError as err:
|
||||
_LOGGER.error('Error processing %s: %s', text, err)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
|
||||
|
||||
hass.http.register_view(ConversationProcessView)
|
||||
|
||||
hass.helpers.intent.async_register(TurnOnIntent())
|
||||
hass.helpers.intent.async_register(TurnOffIntent())
|
||||
async_register(hass, INTENT_TURN_ON,
|
||||
['Turn {name} on', 'Turn on {name}'])
|
||||
async_register(hass, INTENT_TURN_OFF, [
|
||||
'Turn {name} off', 'Turn off {name}'])
|
||||
# We strip trailing 's' from name because our state matcher will fail
|
||||
# if a letter is not there. By removing 's' we can match singular and
|
||||
# plural names.
|
||||
|
||||
async_register(hass, intent.INTENT_TURN_ON, [
|
||||
'Turn [the] [a] {name}[s] on',
|
||||
'Turn on [the] [a] [an] {name}[s]',
|
||||
])
|
||||
async_register(hass, intent.INTENT_TURN_OFF, [
|
||||
'Turn [the] [a] [an] {name}[s] off',
|
||||
'Turn off [the] [a] [an] {name}[s]',
|
||||
])
|
||||
async_register(hass, intent.INTENT_TOGGLE, [
|
||||
'Toggle [the] [a] [an] {name}[s]',
|
||||
'[the] [a] [an] {name}[s] toggle',
|
||||
])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _create_matcher(utterance):
|
||||
"""Create a regex that matches the utterance."""
|
||||
parts = re.split(r'({\w+})', utterance)
|
||||
# Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
|
||||
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
|
||||
parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance)
|
||||
# Pattern to extract name from GROUP part. Matches {name}
|
||||
group_matcher = re.compile(r'{(\w+)}')
|
||||
# Pattern to extract text from OPTIONAL part. Matches [the color]
|
||||
optional_matcher = re.compile(r'\[([\w ]+)\] *')
|
||||
|
||||
pattern = ['^']
|
||||
|
||||
for part in parts:
|
||||
match = group_matcher.match(part)
|
||||
group_match = group_matcher.match(part)
|
||||
optional_match = optional_matcher.match(part)
|
||||
|
||||
if match is None:
|
||||
# Normal part
|
||||
if group_match is None and optional_match is None:
|
||||
pattern.append(part)
|
||||
continue
|
||||
|
||||
pattern.append('(?P<{}>{})'.format(match.groups()[0], r'[\w ]+'))
|
||||
# Group part
|
||||
if group_match is not None:
|
||||
pattern.append(
|
||||
r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0]))
|
||||
|
||||
# Optional part
|
||||
elif optional_match is not None:
|
||||
pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0]))
|
||||
|
||||
pattern.append('$')
|
||||
return re.compile(''.join(pattern), re.I)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _process(hass, text):
|
||||
async def _process(hass, text):
|
||||
"""Process a line of text."""
|
||||
intents = hass.data.get(DOMAIN, {})
|
||||
|
||||
@@ -144,101 +159,31 @@ def _process(hass, text):
|
||||
if not match:
|
||||
continue
|
||||
|
||||
response = yield from hass.helpers.intent.async_handle(
|
||||
response = await hass.helpers.intent.async_handle(
|
||||
DOMAIN, intent_type,
|
||||
{key: {'value': value} for key, value
|
||||
in match.groupdict().items()}, text)
|
||||
return response
|
||||
|
||||
|
||||
@core.callback
|
||||
def _match_entity(hass, name):
|
||||
"""Match a name to an entity."""
|
||||
from fuzzywuzzy import process as fuzzyExtract
|
||||
entities = {state.entity_id: state.name for state
|
||||
in hass.states.async_all()}
|
||||
entity_id = fuzzyExtract.extractOne(
|
||||
name, entities, score_cutoff=65)[2]
|
||||
return hass.states.get(entity_id) if entity_id else None
|
||||
|
||||
|
||||
class TurnOnIntent(intent.IntentHandler):
|
||||
"""Handle turning item on intents."""
|
||||
|
||||
intent_type = INTENT_TURN_ON
|
||||
slot_schema = {
|
||||
'name': cv.string,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle(self, intent_obj):
|
||||
"""Handle turn on intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
name = slots['name']['value']
|
||||
entity = _match_entity(hass, name)
|
||||
|
||||
if not entity:
|
||||
_LOGGER.error("Could not find entity id for %s", name)
|
||||
return None
|
||||
|
||||
yield from hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
}, blocking=True)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response.async_set_speech(
|
||||
'Turned on {}'.format(entity.name))
|
||||
return response
|
||||
|
||||
|
||||
class TurnOffIntent(intent.IntentHandler):
|
||||
"""Handle turning item off intents."""
|
||||
|
||||
intent_type = INTENT_TURN_OFF
|
||||
slot_schema = {
|
||||
'name': cv.string,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle(self, intent_obj):
|
||||
"""Handle turn off intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
name = slots['name']['value']
|
||||
entity = _match_entity(hass, name)
|
||||
|
||||
if not entity:
|
||||
_LOGGER.error("Could not find entity id for %s", name)
|
||||
return None
|
||||
|
||||
yield from hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
}, blocking=True)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response.async_set_speech(
|
||||
'Turned off {}'.format(entity.name))
|
||||
return response
|
||||
|
||||
|
||||
class ConversationProcessView(http.HomeAssistantView):
|
||||
"""View to retrieve shopping list content."""
|
||||
|
||||
url = '/api/conversation/process'
|
||||
name = "api:conversation:process"
|
||||
|
||||
@http.RequestDataValidator(vol.Schema({
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('text'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Send a request for processing."""
|
||||
hass = request.app['hass']
|
||||
|
||||
intent_result = yield from _process(hass, data['text'])
|
||||
try:
|
||||
intent_result = await _process(hass, data['text'])
|
||||
except intent.IntentHandleError as err:
|
||||
intent_result = intent.IntentResponse()
|
||||
intent_result.async_set_speech(str(err))
|
||||
|
||||
if intent_result is None:
|
||||
intent_result = intent.IntentResponse()
|
||||
|
||||
@@ -150,16 +150,14 @@ def stop_cover_tilt(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for covers."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
await component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_cover_service(service):
|
||||
async def async_handle_cover_service(service):
|
||||
"""Handle calls to the cover services."""
|
||||
covers = component.async_extract_from_service(service)
|
||||
method = SERVICE_TO_METHOD.get(service.service)
|
||||
@@ -169,13 +167,13 @@ def async_setup(hass, config):
|
||||
# call method
|
||||
update_tasks = []
|
||||
for cover in covers:
|
||||
yield from getattr(cover, method['method'])(**params)
|
||||
await getattr(cover, method['method'])(**params)
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
update_tasks.append(cover.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
for service_name in SERVICE_TO_METHOD:
|
||||
schema = SERVICE_TO_METHOD[service_name].get(
|
||||
|
||||
@@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION,
|
||||
ATTR_TILT_POSITION)
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
|
||||
@@ -137,8 +138,9 @@ class DemoCover(CoverDevice):
|
||||
self._listen_cover_tilt()
|
||||
self._requested_closing_tilt = False
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
self._set_position = round(position, -1)
|
||||
if self._position == position:
|
||||
return
|
||||
@@ -146,8 +148,9 @@ class DemoCover(CoverDevice):
|
||||
self._listen_cover()
|
||||
self._requested_closing = position < self._position
|
||||
|
||||
def set_cover_tilt_position(self, tilt_position, **kwargs):
|
||||
def set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover til to a specific position."""
|
||||
tilt_position = kwargs.get(ATTR_TILT_POSITION)
|
||||
self._set_tilt_position = round(tilt_position, -1)
|
||||
if self._tilt_position == tilt_position:
|
||||
return
|
||||
|
||||
@@ -201,21 +201,21 @@ class GaradgetCover(CoverDevice):
|
||||
"""Check the state of the service during an operation."""
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def close_cover(self):
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
if self._state not in ['close', 'closing']:
|
||||
ret = self._put_command('setState', 'close')
|
||||
self._start_watcher('close')
|
||||
return ret.get('return_value') == 1
|
||||
|
||||
def open_cover(self):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
if self._state not in ['open', 'opening']:
|
||||
ret = self._put_command('setState', 'open')
|
||||
self._start_watcher('open')
|
||||
return ret.get('return_value') == 1
|
||||
|
||||
def stop_cover(self):
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the door where it is."""
|
||||
if self._state not in ['stopped']:
|
||||
ret = self._put_command('setState', 'stop')
|
||||
|
||||
@@ -42,10 +42,6 @@ def setup_platform(hass, config: ConfigType,
|
||||
class ISYCoverDevice(ISYDevice, CoverDevice):
|
||||
"""Representation of an ISY994 cover device."""
|
||||
|
||||
def __init__(self, node: object) -> None:
|
||||
"""Initialize the ISY994 cover device."""
|
||||
super().__init__(node)
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int:
|
||||
"""Return the current cover position."""
|
||||
@@ -61,8 +57,7 @@ class ISYCoverDevice(ISYDevice, CoverDevice):
|
||||
"""Get the state of the ISY994 cover device."""
|
||||
if self.is_unknown():
|
||||
return None
|
||||
else:
|
||||
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
|
||||
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
|
||||
|
||||
def open_cover(self, **kwargs) -> None:
|
||||
"""Send the open cover command to the ISY994 cover device."""
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for KNX/IP covers.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.knx/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -50,12 +49,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up cover(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized:
|
||||
return
|
||||
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
else:
|
||||
@@ -109,11 +105,10 @@ class KNXCover(CoverDevice):
|
||||
@callback
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
async def after_update_callback(device):
|
||||
"""Call after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
@@ -150,32 +145,28 @@ class KNXCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
return self.device.is_closed()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
if not self.device.is_closed():
|
||||
yield from self.device.set_down()
|
||||
await self.device.set_down()
|
||||
self.start_auto_updater()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
if not self.device.is_open():
|
||||
yield from self.device.set_up()
|
||||
await self.device.set_up()
|
||||
self.start_auto_updater()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
yield from self.device.set_position(position)
|
||||
await self.device.set_position(position)
|
||||
self.start_auto_updater()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
yield from self.device.stop()
|
||||
await self.device.stop()
|
||||
self.stop_auto_updater()
|
||||
|
||||
@property
|
||||
@@ -185,12 +176,11 @@ class KNXCover(CoverDevice):
|
||||
return None
|
||||
return self.device.current_angle()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
async def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if ATTR_TILT_POSITION in kwargs:
|
||||
tilt_position = kwargs[ATTR_TILT_POSITION]
|
||||
yield from self.device.set_angle(tilt_position)
|
||||
await self.device.set_angle(tilt_position)
|
||||
|
||||
def start_auto_updater(self):
|
||||
"""Start the autoupdater to update HASS while cover is moving."""
|
||||
|
||||
@@ -65,9 +65,9 @@ TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_COMMAND_TOPIC, default=None): valid_publish_topic,
|
||||
vol.Optional(CONF_POSITION_TOPIC, default=None): valid_publish_topic,
|
||||
vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_POSITION_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
@@ -78,8 +78,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
|
||||
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_TILT_COMMAND_TOPIC, default=None): valid_publish_topic,
|
||||
vol.Optional(CONF_TILT_STATUS_TOPIC, default=None): valid_subscribe_topic,
|
||||
vol.Optional(CONF_TILT_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_TILT_STATUS_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_TILT_CLOSED_POSITION,
|
||||
default=DEFAULT_TILT_CLOSED_POSITION): int,
|
||||
vol.Optional(CONF_TILT_OPEN_POSITION,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user