mirror of
https://github.com/home-assistant/core.git
synced 2026-01-08 00:28:31 +01:00
Compare commits
387 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ae6a3402c | ||
|
|
bd51143ac1 | ||
|
|
0a25d30ba6 | ||
|
|
bddfe24753 | ||
|
|
1d7ecc22f9 | ||
|
|
703eea0c93 | ||
|
|
b70b23ef83 | ||
|
|
1a9727c75a | ||
|
|
e6006e9beb | ||
|
|
4008bf5611 | ||
|
|
edcb242b6d | ||
|
|
0081764ddc | ||
|
|
bb5484edac | ||
|
|
63820a78d9 | ||
|
|
61a3b4ffdb | ||
|
|
1b3c3494e8 | ||
|
|
b2d37f5257 | ||
|
|
206e38a2ab | ||
|
|
fe56844a3a | ||
|
|
692b2644c7 | ||
|
|
f263a931f7 | ||
|
|
301077ded9 | ||
|
|
415af5e257 | ||
|
|
9ce02d2717 | ||
|
|
4b2fdd243a | ||
|
|
032d6963d8 | ||
|
|
13bda2669e | ||
|
|
568c6c16fa | ||
|
|
92bd932679 | ||
|
|
bfb49c2e58 | ||
|
|
89f5a938c7 | ||
|
|
9ce4755f8a | ||
|
|
b342c43b09 | ||
|
|
95e98925d1 | ||
|
|
53f08e313f | ||
|
|
9fb73c1bab | ||
|
|
98e4d514a5 | ||
|
|
79eb75f26f | ||
|
|
ff960c0c7a | ||
|
|
52d2139904 | ||
|
|
e687ca781f | ||
|
|
ff72c5e456 | ||
|
|
be43c3bcfe | ||
|
|
4ad0152a44 | ||
|
|
eb763764b3 | ||
|
|
cd96d7b2ec | ||
|
|
dee47d50ec | ||
|
|
c8f2810fac | ||
|
|
ff9f500c51 | ||
|
|
0c0e0c36af | ||
|
|
a8fdd76f44 | ||
|
|
45ef34ff81 | ||
|
|
343d1384a3 | ||
|
|
9f0f7394fb | ||
|
|
5fce2e2b47 | ||
|
|
8fbef5b002 | ||
|
|
3e082b5ce6 | ||
|
|
7c99567b65 | ||
|
|
7b3d17bae4 | ||
|
|
12affa1469 | ||
|
|
477f7ec01e | ||
|
|
7bf8d4ab12 | ||
|
|
3b4faa74a0 | ||
|
|
2518587534 | ||
|
|
273a43be02 | ||
|
|
72fb64695e | ||
|
|
bf44dc422c | ||
|
|
bf58945680 | ||
|
|
0cfc7256ac | ||
|
|
0d62f472cb | ||
|
|
ad5a11ba3d | ||
|
|
4dea55b29c | ||
|
|
0f2cfe7f27 | ||
|
|
9fc8a8f679 | ||
|
|
f40efe0110 | ||
|
|
c361b0c450 | ||
|
|
0f24fea6bb | ||
|
|
0911166c9c | ||
|
|
8fad97a47a | ||
|
|
9cfcd38c1e | ||
|
|
979a8f8772 | ||
|
|
6314aabc6f | ||
|
|
931bceefd9 | ||
|
|
78f3e01854 | ||
|
|
5801d78017 | ||
|
|
184f2be83e | ||
|
|
1ae8b6ee08 | ||
|
|
170763ef2f | ||
|
|
507c658fe9 | ||
|
|
3e5462ebff | ||
|
|
8617177ff1 | ||
|
|
df78eecc1b | ||
|
|
5908b55bba | ||
|
|
e5d76c53fb | ||
|
|
f26aff4885 | ||
|
|
32b0712089 | ||
|
|
867010240a | ||
|
|
0428559f69 | ||
|
|
dfd15900c7 | ||
|
|
e993d095cb | ||
|
|
26fb3d7faa | ||
|
|
b0073b437f | ||
|
|
e04b01daad | ||
|
|
020669fc60 | ||
|
|
d897a07d0b | ||
|
|
a6b63b669e | ||
|
|
ab9b915731 | ||
|
|
0a0b33af03 | ||
|
|
f391cbae27 | ||
|
|
27865f58f1 | ||
|
|
3fdb0002a7 | ||
|
|
298e6eeef1 | ||
|
|
cea2de5eb5 | ||
|
|
3b537f6e2a | ||
|
|
ef7fd9f380 | ||
|
|
b3b7cf3fa7 | ||
|
|
45ff15bc85 | ||
|
|
bdb4d754ae | ||
|
|
00c6df54b2 | ||
|
|
2bebfec3a6 | ||
|
|
264be67787 | ||
|
|
06aded1a4d | ||
|
|
9eda04b787 | ||
|
|
8a0facb747 | ||
|
|
81cf0dacfe | ||
|
|
e1d2d201c8 | ||
|
|
3639a4470c | ||
|
|
94d9aa0c5f | ||
|
|
a06eea444a | ||
|
|
ce3a5972c7 | ||
|
|
f48ce3d437 | ||
|
|
dfe3219f3f | ||
|
|
254256c08f | ||
|
|
f1d37fc849 | ||
|
|
08bcf84170 | ||
|
|
24e0bb198a | ||
|
|
263dbe5d81 | ||
|
|
3e6f4d0e5a | ||
|
|
181e68b027 | ||
|
|
2e3ec121d1 | ||
|
|
a6e455a070 | ||
|
|
a08e5efe53 | ||
|
|
a507ed0af8 | ||
|
|
068b037944 | ||
|
|
38d2702e3c | ||
|
|
93b9ec0b0f | ||
|
|
22cefc7e64 | ||
|
|
60f6109cbf | ||
|
|
24d299e266 | ||
|
|
444805df10 | ||
|
|
a08293cff7 | ||
|
|
0d48a8eec6 | ||
|
|
8a204fd15b | ||
|
|
1887bac37e | ||
|
|
d6af26b589 | ||
|
|
a5ae77ab93 | ||
|
|
8ab5978db3 | ||
|
|
3e3f710b12 | ||
|
|
6d20a84f0e | ||
|
|
b996632965 | ||
|
|
eaf81150ea | ||
|
|
7db37a3834 | ||
|
|
55daea5169 | ||
|
|
594a5b7d29 | ||
|
|
3a765692e7 | ||
|
|
2d2e8034d6 | ||
|
|
f3ccbda054 | ||
|
|
7166d53e2b | ||
|
|
e36f27d6fd | ||
|
|
11930d5f20 | ||
|
|
df35159cb4 | ||
|
|
4d52875229 | ||
|
|
8bd5f66c57 | ||
|
|
872b6cf16b | ||
|
|
8e14e803cb | ||
|
|
101b39300b | ||
|
|
b159484a79 | ||
|
|
725e1ddfc1 | ||
|
|
a17e60208d | ||
|
|
6a625bdb37 | ||
|
|
630734ca15 | ||
|
|
7fd687f59c | ||
|
|
4bd6776443 | ||
|
|
2532d67b9a | ||
|
|
df8596e896 | ||
|
|
2497dd5e33 | ||
|
|
553920780f | ||
|
|
8852e52601 | ||
|
|
23165cbd1a | ||
|
|
23f06b0040 | ||
|
|
5ec6f25d4e | ||
|
|
79c9d3ba10 | ||
|
|
ba7178dc0c | ||
|
|
2c7bc6eaf8 | ||
|
|
c50b00226c | ||
|
|
fb1fafefab | ||
|
|
98620d8ce8 | ||
|
|
d385e9645d | ||
|
|
e9cdbe5d8c | ||
|
|
6e75c5427c | ||
|
|
1676df6a5f | ||
|
|
17cbd0f3c9 | ||
|
|
2d7d8848cb | ||
|
|
3939460814 | ||
|
|
36bc7f8175 | ||
|
|
0396725fe9 | ||
|
|
c3974e540b | ||
|
|
74c249e57d | ||
|
|
da4e630f54 | ||
|
|
ffbafa687a | ||
|
|
7718f70c5f | ||
|
|
0de2681783 | ||
|
|
7e08e8bd51 | ||
|
|
2388d62755 | ||
|
|
fab958d789 | ||
|
|
f8127a3902 | ||
|
|
3426487277 | ||
|
|
cfb0b00c0c | ||
|
|
852eef8046 | ||
|
|
05c9c57500 | ||
|
|
5c4529d044 | ||
|
|
3fa080a795 | ||
|
|
0977be1842 | ||
|
|
4270bc7abb | ||
|
|
a04c6d5830 | ||
|
|
f287955422 | ||
|
|
2bc7e58780 | ||
|
|
947218d51c | ||
|
|
49683181d1 | ||
|
|
89c7c80e42 | ||
|
|
6b059489a6 | ||
|
|
1cbf9792d7 | ||
|
|
437ffc8337 | ||
|
|
9cb3c9034f | ||
|
|
1dcc51cbdf | ||
|
|
022d8fb816 | ||
|
|
1e17b2fd63 | ||
|
|
b45dad507a | ||
|
|
8ed3024026 | ||
|
|
d042b3d7d1 | ||
|
|
4d3743f3f7 | ||
|
|
181eca4b45 | ||
|
|
dbc59ad1a7 | ||
|
|
82f59ba984 | ||
|
|
d35077271d | ||
|
|
aec61b7c86 | ||
|
|
e01a0f91d6 | ||
|
|
8fed405da7 | ||
|
|
3442b6741d | ||
|
|
f5093b474a | ||
|
|
05676ba18b | ||
|
|
66c6f9cdd6 | ||
|
|
5a9013cda5 | ||
|
|
d78e75db66 | ||
|
|
d04ba3f86d | ||
|
|
ed6cd0ccfa | ||
|
|
88d2a6ab80 | ||
|
|
fe7012549e | ||
|
|
f013619e69 | ||
|
|
78144bc6de | ||
|
|
f6ae2d338d | ||
|
|
99f7e2bd97 | ||
|
|
b1079cb493 | ||
|
|
0deef34881 | ||
|
|
2350ce96a6 | ||
|
|
de1ff1e952 | ||
|
|
d13bcf8412 | ||
|
|
456ff4e84b | ||
|
|
89a19c89a7 | ||
|
|
a86bf81768 | ||
|
|
5e675677ad | ||
|
|
ff416c0e7a | ||
|
|
170b8671b9 | ||
|
|
ee6d6a8859 | ||
|
|
1d2fd8a2e9 | ||
|
|
92f13ff60d | ||
|
|
5c434f143e | ||
|
|
646ed5de52 | ||
|
|
27c1806897 | ||
|
|
6909be1cc7 | ||
|
|
223bc187dc | ||
|
|
c971d61422 | ||
|
|
e122692b46 | ||
|
|
76874e1cbc | ||
|
|
d348f09d3d | ||
|
|
64f18c62f4 | ||
|
|
be2e202618 | ||
|
|
07f20676cb | ||
|
|
a30ca4307b | ||
|
|
8d52eba484 | ||
|
|
8e05a5c12b | ||
|
|
25fe6ec536 | ||
|
|
30a1fedce8 | ||
|
|
4e569ac0c3 | ||
|
|
8a6370f7c9 | ||
|
|
874cccd530 | ||
|
|
e1a5e5a8ba | ||
|
|
a9917e7a56 | ||
|
|
ef7ce5eb1b | ||
|
|
7fc9ac0931 | ||
|
|
e2029e3970 | ||
|
|
7e2fc19f5a | ||
|
|
c48c8710b7 | ||
|
|
b6bed1dfab | ||
|
|
948f29544a | ||
|
|
6310deb5c2 | ||
|
|
cfded7eab9 | ||
|
|
0ef4340099 | ||
|
|
24a9da85c0 | ||
|
|
71baa6532e | ||
|
|
4c9e7c2da4 | ||
|
|
5958e6a60f | ||
|
|
3e7a737bff | ||
|
|
dd48fb04a3 | ||
|
|
d5612b5ccc | ||
|
|
8a1687accb | ||
|
|
53351423dd | ||
|
|
75fb8ef98b | ||
|
|
989638b266 | ||
|
|
d028c33e7f | ||
|
|
0a2e949e0a | ||
|
|
1bea8747ac | ||
|
|
e54394e906 | ||
|
|
c384fd9653 | ||
|
|
3560fa754c | ||
|
|
101a6ab07c | ||
|
|
eb1ca20cfc | ||
|
|
ae286a550b | ||
|
|
c5330a13b6 | ||
|
|
6ab4a408d2 | ||
|
|
1202134964 | ||
|
|
bbbb44b999 | ||
|
|
54e0cc1304 | ||
|
|
f9e07e617c | ||
|
|
95a528a75f | ||
|
|
51b0cbefe3 | ||
|
|
8d8b07abd5 | ||
|
|
15d345c4ef | ||
|
|
676c94561b | ||
|
|
02ad9c3574 | ||
|
|
890197e407 | ||
|
|
9ee123f5ce | ||
|
|
14aa4e7694 | ||
|
|
991c457430 | ||
|
|
401e92f84e | ||
|
|
1dc5fa145f | ||
|
|
dff4f6ce48 | ||
|
|
56b3cb0583 | ||
|
|
d0f089975d | ||
|
|
26960283a0 | ||
|
|
f5cc40024d | ||
|
|
d42b5a93dd | ||
|
|
3dfc49d311 | ||
|
|
dc8424032b | ||
|
|
f164a5a65f | ||
|
|
d74a2b68c1 | ||
|
|
458598546d | ||
|
|
3f6d30ed06 | ||
|
|
28ff1f7ac2 | ||
|
|
0e00de8a33 | ||
|
|
ae47da7bce | ||
|
|
f01b5b0040 | ||
|
|
40485a6e89 | ||
|
|
7ea7fc8d38 | ||
|
|
86baed4e52 | ||
|
|
b4b779c49d | ||
|
|
0143752d94 | ||
|
|
e910ecfd5f | ||
|
|
4d74fc2d07 | ||
|
|
f9c1675c95 | ||
|
|
76fb2447a5 | ||
|
|
2fae86bbd3 | ||
|
|
3ca139e21e | ||
|
|
a8a895a61b | ||
|
|
36361d623d | ||
|
|
652e0d45a9 | ||
|
|
556901ea48 | ||
|
|
05255b9c3f | ||
|
|
11e1b8a19d | ||
|
|
37d8cd7b75 | ||
|
|
d8a7c547df | ||
|
|
ecaf0189cc | ||
|
|
ca5f470956 | ||
|
|
3ba19c52d5 | ||
|
|
6734c966b3 | ||
|
|
5e2296f2a4 | ||
|
|
c5228cbc64 |
49
.coveragerc
49
.coveragerc
@@ -109,6 +109,9 @@ omit =
|
||||
homeassistant/components/homematic/__init__.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/homematicip_cloud.py
|
||||
homeassistant/components/*/homematicip_cloud.py
|
||||
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/*/ihc.py
|
||||
|
||||
@@ -157,9 +160,6 @@ omit =
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/mercedesme.py
|
||||
homeassistant/components/*/mercedesme.py
|
||||
|
||||
homeassistant/components/mochad.py
|
||||
homeassistant/components/*/mochad.py
|
||||
|
||||
@@ -286,11 +286,9 @@ omit =
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/xiaomi_aqara.py
|
||||
homeassistant/components/binary_sensor/xiaomi_aqara.py
|
||||
homeassistant/components/cover/xiaomi_aqara.py
|
||||
homeassistant/components/light/xiaomi_aqara.py
|
||||
homeassistant/components/sensor/xiaomi_aqara.py
|
||||
homeassistant/components/switch/xiaomi_aqara.py
|
||||
homeassistant/components/*/xiaomi_aqara.py
|
||||
|
||||
homeassistant/components/*/xiaomi_miio.py
|
||||
|
||||
homeassistant/components/zabbix.py
|
||||
homeassistant/components/*/zabbix.py
|
||||
@@ -309,6 +307,7 @@ omit =
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/ialarm.py
|
||||
homeassistant/components/alarm_control_panel/ifttt.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
@@ -332,6 +331,7 @@ omit =
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/proxy.py
|
||||
homeassistant/components/camera/ring.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
@@ -352,6 +352,7 @@ omit =
|
||||
homeassistant/components/climate/touchline.py
|
||||
homeassistant/components/climate/venstar.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/gogogate2.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/knx.py
|
||||
homeassistant/components/cover/myq.py
|
||||
@@ -369,6 +370,7 @@ omit =
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/google_maps.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/hitron_coda.py
|
||||
homeassistant/components/device_tracker/huawei_router.py
|
||||
@@ -395,28 +397,29 @@ omit =
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/fan/xiaomi_miio.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/folder_watcher.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/goalfeed.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/image_processing/dlib_face_detect.py
|
||||
homeassistant/components/image_processing/dlib_face_identify.py
|
||||
homeassistant/components/image_processing/seven_segments.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/aurora.py
|
||||
homeassistant/components/light/avion.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/blinkt.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/decora_wifi.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/greenwave.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/iglo.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
@@ -427,7 +430,6 @@ omit =
|
||||
homeassistant/components/light/tplink.py
|
||||
homeassistant/components/light/tradfri.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/xiaomi_miio.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/zengge.py
|
||||
@@ -436,12 +438,14 @@ omit =
|
||||
homeassistant/components/lock/nello.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/map.py
|
||||
homeassistant/components/media_extractor.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/bluesound.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/channels.py
|
||||
homeassistant/components/media_player/clementine.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
@@ -482,8 +486,8 @@ omit =
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/xiaomi_tv.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/media_player/yamaha_musiccast.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/media_player/ziggo_mediabox_xl.py
|
||||
homeassistant/components/mycroft.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
@@ -491,8 +495,8 @@ omit =
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/clickatell.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
@@ -502,6 +506,7 @@ omit =
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/mastodon.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/mycroft.py
|
||||
@@ -518,6 +523,7 @@ omit =
|
||||
homeassistant/components/notify/simplepush.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/stride.py
|
||||
homeassistant/components/notify/synology_chat.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
@@ -531,7 +537,6 @@ omit =
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/remote/xiaomi_miio.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
@@ -554,7 +559,6 @@ omit =
|
||||
homeassistant/components/sensor/crimereports.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deluge.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
@@ -576,6 +580,7 @@ omit =
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
homeassistant/components/sensor/foobot.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gearbest.py
|
||||
@@ -588,8 +593,8 @@ omit =
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/irish_rail_transport.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
@@ -632,8 +637,8 @@ omit =
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/shodan.py
|
||||
homeassistant/components/sensor/simulated.py
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
@@ -647,6 +652,7 @@ omit =
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/syncthru.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/sytadin.py
|
||||
@@ -656,6 +662,7 @@ omit =
|
||||
homeassistant/components/sensor/tibber.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/trafikverket_weatherstation.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/travisci.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
@@ -665,6 +672,7 @@ omit =
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/viaggiatreno.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/waze_travel_time.py
|
||||
homeassistant/components/sensor/whois.py
|
||||
homeassistant/components/sensor/worldtidesinfo.py
|
||||
homeassistant/components/sensor/worxlandroid.py
|
||||
@@ -697,7 +705,7 @@ omit =
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/xiaomi_miio.py
|
||||
homeassistant/components/switch/vesync.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
@@ -706,7 +714,6 @@ omit =
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
homeassistant/components/vacuum/roomba.py
|
||||
homeassistant/components/vacuum/xiaomi_miio.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
homeassistant/components/weather/darksky.py
|
||||
|
||||
38
.github/ISSUE_TEMPLATE.md
vendored
38
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,35 +1,45 @@
|
||||
Make sure you are running the latest version of Home Assistant before reporting an issue.
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||
-->
|
||||
|
||||
You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum:
|
||||
|
||||
**Home Assistant release (`hass --version`):**
|
||||
**Home Assistant release with the issue:**
|
||||
<!--
|
||||
- Frontend -> Developer tools -> Info
|
||||
- Or use this command: hass --version
|
||||
-->
|
||||
|
||||
|
||||
**Python release (`python3 --version`):**
|
||||
**Last working Home Assistant release (if known):**
|
||||
|
||||
|
||||
**Operating environment (Hass.io/Docker/Windows/etc.):**
|
||||
<!--
|
||||
Please provide details about your environment.
|
||||
-->
|
||||
|
||||
**Component/platform:**
|
||||
<!--
|
||||
Please add the link to the documentation at https://www.home-assistant.io/components/ of the component/platform in question.
|
||||
-->
|
||||
|
||||
|
||||
**Description of problem:**
|
||||
|
||||
|
||||
**Expected:**
|
||||
|
||||
|
||||
**Problem-relevant `configuration.yaml` entries and steps to reproduce:**
|
||||
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Traceback (if applicable):**
|
||||
```bash
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
**Additional info:**
|
||||
**Additional information:**
|
||||
|
||||
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -12,19 +12,18 @@
|
||||
|
||||
## Checklist:
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
|
||||
If the code communicates with devices, web services, or third-party tools:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
- [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New files were added to `.coveragerc`.
|
||||
|
||||
If the code does not interact with devices:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,6 +21,7 @@ Icon
|
||||
*.iml
|
||||
|
||||
# pytest
|
||||
.pytest_cache
|
||||
.cache
|
||||
|
||||
# GITHUB Proposed Python stuff:
|
||||
|
||||
0
.gitmodules
vendored
0
.gitmodules
vendored
21
CODEOWNERS
21
CODEOWNERS
@@ -29,9 +29,6 @@ homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/websocket_api.py @home-assistant/core
|
||||
homeassistant/components/zone.py @home-assistant/core
|
||||
|
||||
# To monitor non-pypi additions
|
||||
requirements_all.txt @andrey-git
|
||||
|
||||
# HomeAssistant developer Teams
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
@@ -43,12 +40,14 @@ homeassistant/components/hassio.py @home-assistant/hassio
|
||||
|
||||
# Individual components
|
||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
||||
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/group.py @cdce8p
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
@@ -68,8 +67,10 @@ homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/sma.py @kellerza
|
||||
homeassistant/components/sensor/sql.py @dgomes
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/switch/rainmachine.py @bachya
|
||||
@@ -79,17 +80,17 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/axis.py @kane610
|
||||
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/eight_sleep.py @mezz64
|
||||
homeassistant/components/*/eight_sleep.py @mezz64
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
homeassistant/components/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
@@ -97,5 +98,9 @@ homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tradfri.py @ggravlingen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||
|
||||
homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
@@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
||||
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
|
||||
- Write the code for your device, notification service, sensor, or IoT thing.
|
||||
- Ensure tests work.
|
||||
|
||||
@@ -4,10 +4,10 @@ homeassistant.util package
|
||||
Submodules
|
||||
----------
|
||||
|
||||
homeassistant.util.async module
|
||||
homeassistant.util.async_ module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: homeassistant.util.async
|
||||
.. automodule:: homeassistant.util.async_
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -272,7 +272,7 @@ def setup_and_run_hass(config_dir: str,
|
||||
|
||||
if args.open_ui:
|
||||
# Imported here to avoid importing asyncio before monkey patch
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
def open_browser(event):
|
||||
"""Open the webinterface in a browser."""
|
||||
@@ -335,7 +335,8 @@ def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
if os.environ.get('HASS_NO_MONKEY') != '1':
|
||||
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
|
||||
if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1':
|
||||
if sys.version_info[:2] >= (3, 6):
|
||||
monkey_patch.disable_c_asyncio()
|
||||
monkey_patch.patch_weakref_tasks()
|
||||
|
||||
@@ -86,14 +86,6 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
if enable_log:
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file)
|
||||
|
||||
if sys.version_info[:2] < (3, 5):
|
||||
_LOGGER.warning(
|
||||
'Python 3.4 support has been deprecated and will be removed in '
|
||||
'the beginning of 2018. Please upgrade Python or your operating '
|
||||
'system. More info: https://home-assistant.io/blog/2017/10/06/'
|
||||
'deprecating-python-3.4-support/'
|
||||
)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.12.2']
|
||||
REQUIREMENTS = ['abodepy==0.12.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -12,13 +12,14 @@ import requests
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED,
|
||||
STATE_ALARM_ARMED_NIGHT)
|
||||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE, EGARDIA_SERVER,
|
||||
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
|
||||
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
|
||||
)
|
||||
REQUIREMENTS = ['pythonegardia==1.0.38']
|
||||
DEPENDENCIES = ['egardia']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,12 +28,16 @@ STATES = {
|
||||
'DAY HOME': STATE_ALARM_ARMED_HOME,
|
||||
'DISARM': STATE_ALARM_DISARMED,
|
||||
'ARMHOME': STATE_ALARM_ARMED_HOME,
|
||||
'HOME': STATE_ALARM_ARMED_HOME,
|
||||
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
|
||||
'TRIGGERED': STATE_ALARM_TRIGGERED
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Egardia platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
device = EgardiaAlarm(
|
||||
discovery_info['name'],
|
||||
hass.data[EGARDIA_DEVICE],
|
||||
|
||||
170
homeassistant/components/alarm_control_panel/ifttt.py
Normal file
170
homeassistant/components/alarm_control_panel/ifttt.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Interfaces with alarm control panels that have to be controlled through IFTTT.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.ifttt/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
DOMAIN, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.ifttt import (
|
||||
ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE,
|
||||
CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['ifttt']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_STATES = [
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME]
|
||||
|
||||
DATA_IFTTT_ALARM = 'ifttt_alarm'
|
||||
DEFAULT_NAME = "Home"
|
||||
|
||||
CONF_EVENT_AWAY = "event_arm_away"
|
||||
CONF_EVENT_HOME = "event_arm_home"
|
||||
CONF_EVENT_NIGHT = "event_arm_night"
|
||||
CONF_EVENT_DISARM = "event_disarm"
|
||||
|
||||
DEFAULT_EVENT_AWAY = "alarm_arm_away"
|
||||
DEFAULT_EVENT_HOME = "alarm_arm_home"
|
||||
DEFAULT_EVENT_NIGHT = "alarm_arm_night"
|
||||
DEFAULT_EVENT_DISARM = "alarm_disarm"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string,
|
||||
vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string,
|
||||
vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string,
|
||||
vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state"
|
||||
|
||||
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_STATE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a control panel managed through IFTTT."""
|
||||
if DATA_IFTTT_ALARM not in hass.data:
|
||||
hass.data[DATA_IFTTT_ALARM] = []
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
event_away = config.get(CONF_EVENT_AWAY)
|
||||
event_home = config.get(CONF_EVENT_HOME)
|
||||
event_night = config.get(CONF_EVENT_NIGHT)
|
||||
event_disarm = config.get(CONF_EVENT_DISARM)
|
||||
optimistic = config.get(CONF_OPTIMISTIC)
|
||||
|
||||
alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home,
|
||||
event_night, event_disarm, optimistic)
|
||||
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
|
||||
add_devices([alarmpanel])
|
||||
|
||||
async def push_state_update(service):
|
||||
"""Set the service state as device state attribute."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
state = service.data.get(ATTR_STATE)
|
||||
devices = hass.data[DATA_IFTTT_ALARM]
|
||||
if entity_ids:
|
||||
devices = [d for d in devices if d.entity_id in entity_ids]
|
||||
|
||||
for device in devices:
|
||||
device.push_alarm_state(state)
|
||||
device.async_schedule_update_ha_state()
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update,
|
||||
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA)
|
||||
|
||||
|
||||
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an alarm control panel controlled through IFTTT."""
|
||||
|
||||
def __init__(self, name, code, event_away, event_home, event_night,
|
||||
event_disarm, optimistic):
|
||||
"""Initialize the alarm control panel."""
|
||||
self._name = name
|
||||
self._code = code
|
||||
self._event_away = event_away
|
||||
self._event_home = event_home
|
||||
self._event_night = event_night
|
||||
self._event_disarm = event_disarm
|
||||
self._optimistic = optimistic
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Notify that this platform return an assumed state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._check_code(code):
|
||||
return
|
||||
self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._check_code(code):
|
||||
return
|
||||
self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._check_code(code):
|
||||
return
|
||||
self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
if not self._check_code(code):
|
||||
return
|
||||
self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def set_alarm_state(self, event, state):
|
||||
"""Call the IFTTT trigger service to change the alarm state."""
|
||||
data = {ATTR_EVENT: event}
|
||||
|
||||
self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data)
|
||||
_LOGGER.debug("Called IFTTT component to trigger event %s", event)
|
||||
if self._optimistic:
|
||||
self._state = state
|
||||
|
||||
def push_alarm_state(self, value):
|
||||
"""Push the alarm state to the given value."""
|
||||
if value in ALLOWED_STATES:
|
||||
_LOGGER.debug("Pushed the alarm state to %s", value)
|
||||
self._state = value
|
||||
|
||||
def _check_code(self, code):
|
||||
return self._code is None or self._code == code
|
||||
@@ -69,3 +69,13 @@ alarmdecoder_alarm_toggle_chime:
|
||||
code:
|
||||
description: A required code to toggle the alarm control panel chime with.
|
||||
example: 1234
|
||||
|
||||
ifttt_push_alarm_state:
|
||||
description: Update the alarm state to the specified value.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel which state has to be updated.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
state:
|
||||
description: The state to which the alarm control panel has to be set.
|
||||
example: 'armed_night'
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.16']
|
||||
REQUIREMENTS = ['total_connect_client==0.17']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -6,18 +6,20 @@ from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from homeassistant.components import (
|
||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
||||
alert, automation, cover, climate, fan, group, input_boolean, light, lock,
|
||||
media_player, scene, script, switch, http, sensor)
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
|
||||
SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
||||
|
||||
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -34,6 +36,16 @@ API_TEMP_UNITS = {
|
||||
TEMP_CELSIUS: 'CELSIUS',
|
||||
}
|
||||
|
||||
API_THERMOSTAT_MODES = {
|
||||
climate.STATE_HEAT: 'HEAT',
|
||||
climate.STATE_COOL: 'COOL',
|
||||
climate.STATE_AUTO: 'AUTO',
|
||||
climate.STATE_ECO: 'ECO',
|
||||
climate.STATE_IDLE: 'OFF',
|
||||
climate.STATE_FAN_ONLY: 'OFF',
|
||||
climate.STATE_DRY: 'OFF',
|
||||
}
|
||||
|
||||
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
||||
|
||||
CONF_DESCRIPTION = 'description'
|
||||
@@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface):
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp = self.entity.state
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
temp = self.entity.attributes.get(
|
||||
climate.ATTR_CURRENT_TEMPERATURE)
|
||||
return {
|
||||
'value': float(self.entity.state),
|
||||
'value': float(temp),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
}
|
||||
|
||||
|
||||
class _AlexaThermostatController(_AlexaInterface):
|
||||
def name(self):
|
||||
return 'Alexa.ThermostatController'
|
||||
|
||||
def properties_supported(self):
|
||||
properties = []
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
|
||||
properties.append({'name': 'targetSetpoint'})
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
|
||||
properties.append({'name': 'lowerSetpoint'})
|
||||
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
|
||||
properties.append({'name': 'upperSetpoint'})
|
||||
if supported & climate.SUPPORT_OPERATION_MODE:
|
||||
properties.append({'name': 'thermostatMode'})
|
||||
return properties
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
if name == 'thermostatMode':
|
||||
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
|
||||
mode = API_THERMOSTAT_MODES.get(ha_mode)
|
||||
if mode is None:
|
||||
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
|
||||
self.entity.entity_id, type(self.entity),
|
||||
climate.ATTR_OPERATION_MODE, ha_mode)
|
||||
raise _UnsupportedProperty(name)
|
||||
return mode
|
||||
|
||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp = None
|
||||
if name == 'targetSetpoint':
|
||||
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
|
||||
elif name == 'lowerSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
|
||||
elif name == 'upperSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
|
||||
if temp is None:
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
return {
|
||||
'value': float(temp),
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
}
|
||||
|
||||
@@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity):
|
||||
return [_AlexaPowerController(self.entity)]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
||||
class _ClimateCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
return [_DisplayCategory.THERMOSTAT]
|
||||
|
||||
def interfaces(self):
|
||||
yield _AlexaThermostatController(self.entity)
|
||||
yield _AlexaTemperatureSensor(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||
class _CoverCapabilities(_AlexaEntity):
|
||||
def default_display_categories(self):
|
||||
@@ -438,9 +512,7 @@ class _LightCapabilities(_AlexaEntity):
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & light.SUPPORT_BRIGHTNESS:
|
||||
yield _AlexaBrightnessController(self.entity)
|
||||
if supported & light.SUPPORT_RGB_COLOR:
|
||||
yield _AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_XY_COLOR:
|
||||
if supported & light.SUPPORT_COLOR:
|
||||
yield _AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_COLOR_TEMP:
|
||||
yield _AlexaColorTemperatureController(self.entity)
|
||||
@@ -684,17 +756,26 @@ def api_message(request,
|
||||
return response
|
||||
|
||||
|
||||
def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
|
||||
def api_error(request,
|
||||
namespace='Alexa',
|
||||
error_type='INTERNAL_ERROR',
|
||||
error_message="",
|
||||
payload=None):
|
||||
"""Create a API formatted error response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
payload = {
|
||||
'type': error_type,
|
||||
'message': error_message,
|
||||
}
|
||||
payload = payload or {}
|
||||
payload['type'] = error_type
|
||||
payload['message'] = error_message
|
||||
|
||||
return api_message(request, name='ErrorResponse', payload=payload)
|
||||
_LOGGER.info("Request %s/%s error %s: %s",
|
||||
request[API_HEADER]['namespace'],
|
||||
request[API_HEADER]['name'],
|
||||
error_type, error_message)
|
||||
|
||||
return api_message(
|
||||
request, name='ErrorResponse', namespace=namespace, payload=payload)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||
@@ -842,25 +923,16 @@ def async_api_adjust_brightness(hass, config, request, entity):
|
||||
@asyncio.coroutine
|
||||
def async_api_set_color(hass, config, request, entity):
|
||||
"""Process a set color request."""
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
rgb = color_util.color_hsb_to_RGB(
|
||||
float(request[API_PAYLOAD]['color']['hue']),
|
||||
float(request[API_PAYLOAD]['color']['saturation']),
|
||||
float(request[API_PAYLOAD]['color']['brightness'])
|
||||
)
|
||||
|
||||
if supported & light.SUPPORT_RGB_COLOR > 0:
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=False)
|
||||
else:
|
||||
xyz = color_util.color_RGB_to_xy(*rgb)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
|
||||
light.ATTR_BRIGHTNESS: xyz[2],
|
||||
}, blocking=False)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -1115,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity):
|
||||
else:
|
||||
msg = 'failed to map input {} to a media source on {}'.format(
|
||||
media_input, entity.entity_id)
|
||||
_LOGGER.error(msg)
|
||||
return api_error(
|
||||
request, error_type='INVALID_VALUE', error_message=msg)
|
||||
|
||||
@@ -1287,6 +1358,149 @@ def async_api_previous(hass, config, request, entity):
|
||||
return api_message(request)
|
||||
|
||||
|
||||
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
|
||||
"""Create temperature value out of range API error response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
temp_range = {
|
||||
'minimumValue': {
|
||||
'value': min_temp,
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
},
|
||||
'maximumValue': {
|
||||
'value': max_temp,
|
||||
'scale': API_TEMP_UNITS[unit],
|
||||
},
|
||||
}
|
||||
|
||||
msg = 'The requested temperature {} is out of range'.format(temp)
|
||||
return api_error(
|
||||
request,
|
||||
error_type='TEMPERATURE_VALUE_OUT_OF_RANGE',
|
||||
error_message=msg,
|
||||
payload={'validRange': temp_range},
|
||||
)
|
||||
|
||||
|
||||
def temperature_from_object(temp_obj, to_unit, interval=False):
|
||||
"""Get temperature from Temperature object in requested unit."""
|
||||
from_unit = TEMP_CELSIUS
|
||||
temp = float(temp_obj['value'])
|
||||
|
||||
if temp_obj['scale'] == 'FAHRENHEIT':
|
||||
from_unit = TEMP_FAHRENHEIT
|
||||
elif temp_obj['scale'] == 'KELVIN':
|
||||
# convert to Celsius if absolute temperature
|
||||
if not interval:
|
||||
temp -= 273.15
|
||||
|
||||
return convert_temperature(temp, from_unit, to_unit, interval)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
|
||||
@extract_entity
|
||||
async def async_api_set_target_temp(hass, config, request, entity):
|
||||
"""Process a set target temperature request."""
|
||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
payload = request[API_PAYLOAD]
|
||||
if 'targetSetpoint' in payload:
|
||||
temp = temperature_from_object(
|
||||
payload['targetSetpoint'], unit)
|
||||
if temp < min_temp or temp > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, temp, min_temp, max_temp, unit)
|
||||
data[ATTR_TEMPERATURE] = temp
|
||||
if 'lowerSetpoint' in payload:
|
||||
temp_low = temperature_from_object(
|
||||
payload['lowerSetpoint'], unit)
|
||||
if temp_low < min_temp or temp_low > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, temp_low, min_temp, max_temp, unit)
|
||||
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
|
||||
if 'upperSetpoint' in payload:
|
||||
temp_high = temperature_from_object(
|
||||
payload['upperSetpoint'], unit)
|
||||
if temp_high < min_temp or temp_high > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, temp_high, min_temp, max_temp, unit)
|
||||
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
|
||||
@extract_entity
|
||||
async def async_api_adjust_target_temp(hass, config, request, entity):
|
||||
"""Process an adjust target temperature request."""
|
||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
|
||||
temp_delta = temperature_from_object(
|
||||
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
|
||||
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
|
||||
|
||||
if target_temp < min_temp or target_temp > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, target_temp, min_temp, max_temp, unit)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
ATTR_TEMPERATURE: target_temp,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
|
||||
@extract_entity
|
||||
async def async_api_set_thermostat_mode(hass, config, request, entity):
|
||||
"""Process a set thermostat mode request."""
|
||||
mode = request[API_PAYLOAD]['thermostatMode']
|
||||
|
||||
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
|
||||
# Work around a pylint false positive due to
|
||||
# https://github.com/PyCQA/pylint/issues/1830
|
||||
# pylint: disable=stop-iteration-return
|
||||
ha_mode = next(
|
||||
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||
None
|
||||
)
|
||||
if ha_mode not in operation_list:
|
||||
msg = 'The requested thermostat mode {} is not supported'.format(mode)
|
||||
return api_error(
|
||||
request,
|
||||
namespace='Alexa.ThermostatController',
|
||||
error_type='UNSUPPORTED_THERMOSTAT_MODE',
|
||||
error_message=msg
|
||||
)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
climate.ATTR_OPERATION_MODE: ha_mode,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
|
||||
blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa', 'ReportState'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -10,14 +10,15 @@ from datetime import timedelta
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
from requests.exceptions import ConnectionError as ConnectError
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.1']
|
||||
REQUIREMENTS = ['amcrest==1.2.2']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -63,6 +64,12 @@ SENSORS = {
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
# Switch types are defined like: Name, icon
|
||||
SWITCHES = {
|
||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@@ -81,6 +88,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -93,14 +102,15 @@ def setup(hass, config):
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
try:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
# pylint: disable=pointless-statement
|
||||
camera.current_time
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
except (ConnectError, ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
@@ -108,12 +118,13 @@ def setup(hass, config):
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
continue
|
||||
|
||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||
name = device.get(CONF_NAME)
|
||||
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
switches = device.get(CONF_SWITCHES)
|
||||
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
|
||||
|
||||
username = device.get(CONF_USERNAME)
|
||||
@@ -143,6 +154,13 @@ def setup(hass, config):
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
if switches:
|
||||
discovery.load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SWITCHES: switches
|
||||
}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
119
homeassistant/components/binary_sensor/bmw_connected_drive.py
Normal file
119
homeassistant/components/binary_sensor/bmw_connected_drive.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Reads vehicle status from BMW connected drive portal.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.bmw_connected_drive/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
||||
|
||||
DEPENDENCIES = ['bmw_connected_drive']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'lids': ['Doors', 'opening'],
|
||||
'windows': ['Windows', 'opening'],
|
||||
'door_lock_state': ['Door lock state', 'safety']
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the BMW sensors."""
|
||||
accounts = hass.data[BMW_DOMAIN]
|
||||
_LOGGER.debug('Found BMW accounts: %s',
|
||||
', '.join([a.name for a in accounts]))
|
||||
devices = []
|
||||
for account in accounts:
|
||||
for vehicle in account.account.vehicles:
|
||||
for key, value in sorted(SENSOR_TYPES.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
"""Representation of a BMW vehicle binary sensor."""
|
||||
|
||||
def __init__(self, account, vehicle, attribute: str, sensor_name,
|
||||
device_class):
|
||||
"""Constructor."""
|
||||
self._account = account
|
||||
self._vehicle = vehicle
|
||||
self._attribute = attribute
|
||||
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
||||
self._sensor_name = sensor_name
|
||||
self._device_class = device_class
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Data update is triggered from BMWConnectedDriveEntity."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the binary sensor."""
|
||||
vehicle_state = self._vehicle.state
|
||||
result = {
|
||||
'car': self._vehicle.name
|
||||
}
|
||||
|
||||
if self._attribute == 'lids':
|
||||
for lid in vehicle_state.lids:
|
||||
result[lid.name] = lid.state.value
|
||||
elif self._attribute == 'windows':
|
||||
for window in vehicle_state.windows:
|
||||
result[window.name] = window.state.value
|
||||
elif self._attribute == 'door_lock_state':
|
||||
result['door_lock_state'] = vehicle_state.door_lock_state.value
|
||||
|
||||
return result
|
||||
|
||||
def update(self):
|
||||
"""Read new state data from the library."""
|
||||
from bimmer_connected.state import LockState
|
||||
vehicle_state = self._vehicle.state
|
||||
|
||||
# device class opening: On means open, Off means closed
|
||||
if self._attribute == 'lids':
|
||||
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
|
||||
self._state = not vehicle_state.all_lids_closed
|
||||
if self._attribute == 'windows':
|
||||
self._state = not vehicle_state.all_windows_closed
|
||||
# device class safety: On means unsafe, Off means safe
|
||||
if self._attribute == 'door_lock_state':
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._state = vehicle_state.door_lock_state not in \
|
||||
[LockState.LOCKED, LockState.SECURED]
|
||||
|
||||
def update_callback(self):
|
||||
"""Schedule a state update."""
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add callback after being added to hass.
|
||||
|
||||
Show latest data after startup.
|
||||
"""
|
||||
self._account.add_update_listener(self.update_callback)
|
||||
@@ -4,8 +4,6 @@ Support for deCONZ binary sensor.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import (
|
||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
|
||||
@@ -15,8 +13,8 @@ from homeassistant.core import callback
|
||||
DEPENDENCIES = ['deconz']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -25,8 +23,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
sensors = hass.data[DATA_DECONZ].sensors
|
||||
entities = []
|
||||
|
||||
for key in sorted(sensors.keys(), key=int):
|
||||
sensor = sensors[key]
|
||||
for sensor in sensors.values():
|
||||
if sensor and sensor.type in DECONZ_BINARY_SENSOR:
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
@@ -39,8 +36,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
"""Set up sensor and add update callback to get data from websocket."""
|
||||
self._sensor = sensor
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self._sensor.register_async_callback(self.async_update_callback)
|
||||
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id
|
||||
@@ -96,9 +92,9 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
from pydeconz.sensor import PRESENCE
|
||||
attr = {
|
||||
ATTR_BATTERY_LEVEL: self._sensor.battery,
|
||||
}
|
||||
if self._sensor.type in PRESENCE:
|
||||
attr = {}
|
||||
if self._sensor.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark:
|
||||
attr['dark'] = self._sensor.dark
|
||||
return attr
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['egardia']
|
||||
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
|
||||
'Door Contact': 'opening',
|
||||
'IR': 'motion'}
|
||||
|
||||
@@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
'motionSensor': 'motion',
|
||||
'doorSensor': 'door',
|
||||
'leakSensor': 'moisture'}
|
||||
'wetLeakSensor': 'moisture'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
address = discovery_info['address']
|
||||
device = plm.devices[address]
|
||||
state_key = discovery_info['state_key']
|
||||
name = device.states[state_key].name
|
||||
if name != 'dryLeakSensor':
|
||||
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
|
||||
device.address.hex, device.states[state_key].name)
|
||||
|
||||
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
|
||||
device.address.hex, device.states[state_key].name)
|
||||
new_entity = InsteonPLMBinarySensor(device, state_key)
|
||||
|
||||
new_entity = InsteonPLMBinarySensor(device, state_key)
|
||||
|
||||
async_add_devices([new_entity])
|
||||
async_add_devices([new_entity])
|
||||
|
||||
|
||||
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
@@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
sensorstate = self._insteon_device_state.value
|
||||
return bool(sensorstate)
|
||||
return bool(self._insteon_device_state.value)
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
"""
|
||||
Support for Mercedes cars with Mercedes ME.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mercedesme/
|
||||
"""
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.mercedesme import (
|
||||
DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS)
|
||||
|
||||
DEPENDENCIES = ['mercedesme']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the sensor platform."""
|
||||
data = hass.data[DATA_MME].data
|
||||
|
||||
if not data.cars:
|
||||
_LOGGER.error("No cars found. Check component log.")
|
||||
return
|
||||
|
||||
devices = []
|
||||
for car in data.cars:
|
||||
for key, value in sorted(BINARY_SENSORS.items()):
|
||||
if car['availabilities'].get(key, 'INVALID') == 'VALID':
|
||||
devices.append(MercedesMEBinarySensor(
|
||||
data, key, value[0], car["vin"], None))
|
||||
else:
|
||||
_LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._internal_name == "windowsClosed":
|
||||
return {
|
||||
"window_front_left": self._car["windowStatusFrontLeft"],
|
||||
"window_front_right": self._car["windowStatusFrontRight"],
|
||||
"window_rear_left": self._car["windowStatusRearLeft"],
|
||||
"window_rear_right": self._car["windowStatusRearRight"],
|
||||
"original_value": self._car[self._internal_name],
|
||||
"last_update": datetime.datetime.fromtimestamp(
|
||||
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"car": self._car["license"]
|
||||
}
|
||||
elif self._internal_name == "tireWarningLight":
|
||||
return {
|
||||
"front_right_tire_pressure_kpa":
|
||||
self._car["frontRightTirePressureKpa"],
|
||||
"front_left_tire_pressure_kpa":
|
||||
self._car["frontLeftTirePressureKpa"],
|
||||
"rear_right_tire_pressure_kpa":
|
||||
self._car["rearRightTirePressureKpa"],
|
||||
"rear_left_tire_pressure_kpa":
|
||||
self._car["rearLeftTirePressureKpa"],
|
||||
"original_value": self._car[self._internal_name],
|
||||
"last_update": datetime.datetime.fromtimestamp(
|
||||
self._car["lastUpdate"]
|
||||
).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"car": self._car["license"],
|
||||
}
|
||||
return {
|
||||
"original_value": self._car[self._internal_name],
|
||||
"last_update": datetime.datetime.fromtimestamp(
|
||||
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"car": self._car["license"]
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
self._car = next(
|
||||
car for car in self._data.cars if car["vin"] == self._vin)
|
||||
|
||||
if self._internal_name == "windowsClosed":
|
||||
self._state = bool(self._car[self._internal_name] == "CLOSED")
|
||||
elif self._internal_name == "tireWarningLight":
|
||||
self._state = bool(self._car[self._internal_name] != "INACTIVE")
|
||||
else:
|
||||
self._state = self._car[self._internal_name] is True
|
||||
|
||||
_LOGGER.debug("Updated %s Value: %s IsOn: %s",
|
||||
self._internal_name, self._state, self.is_on)
|
||||
@@ -9,12 +9,24 @@ from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES, DOMAIN, BinarySensorDevice)
|
||||
from homeassistant.const import STATE_ON
|
||||
|
||||
SENSORS = {
|
||||
'S_DOOR': 'door',
|
||||
'S_MOTION': 'motion',
|
||||
'S_SMOKE': 'smoke',
|
||||
'S_SPRINKLER': 'safety',
|
||||
'S_WATER_LEAK': 'safety',
|
||||
'S_SOUND': 'sound',
|
||||
'S_VIBRATION': 'vibration',
|
||||
'S_MOISTURE': 'moisture',
|
||||
}
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the MySensors platform for binary sensors."""
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the mysensors platform for binary sensors."""
|
||||
mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsBinarySensor,
|
||||
add_devices=add_devices)
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
||||
@@ -29,18 +41,7 @@ class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
pres = self.gateway.const.Presentation
|
||||
class_map = {
|
||||
pres.S_DOOR: 'opening',
|
||||
pres.S_MOTION: 'motion',
|
||||
pres.S_SMOKE: 'smoke',
|
||||
}
|
||||
if float(self.gateway.protocol_version) >= 1.5:
|
||||
class_map.update({
|
||||
pres.S_SPRINKLER: 'sprinkler',
|
||||
pres.S_WATER_LEAK: 'leak',
|
||||
pres.S_SOUND: 'sound',
|
||||
pres.S_VIBRATION: 'vibration',
|
||||
pres.S_MOISTURE: 'moisture',
|
||||
})
|
||||
if class_map.get(self.child_type) in DEVICE_CLASSES:
|
||||
return class_map.get(self.child_type)
|
||||
device_class = SENSORS.get(pres(self.child_type).name)
|
||||
if device_class in DEVICE_CLASSES:
|
||||
return device_class
|
||||
return None
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.14.0']
|
||||
REQUIREMENTS = ['numpy==1.14.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.9.3']
|
||||
REQUIREMENTS = ['holidays==0.9.4']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
@@ -30,8 +30,8 @@ ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
|
||||
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
||||
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
|
||||
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
|
||||
'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US',
|
||||
'Wales']
|
||||
'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
|
||||
'UnitedStates', 'US', 'Wales']
|
||||
CONF_COUNTRY = 'country'
|
||||
CONF_PROVINCE = 'province'
|
||||
CONF_WORKDAYS = 'workdays'
|
||||
@@ -47,13 +47,13 @@ DEFAULT_OFFSET = 0
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
|
||||
vol.Optional(CONF_PROVINCE): cv.string,
|
||||
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
|
||||
vol.Optional(CONF_PROVINCE): cv.string,
|
||||
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
})
|
||||
|
||||
|
||||
@@ -74,14 +74,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if province:
|
||||
# 'state' and 'prov' are not interchangeable, so need to make
|
||||
# sure we use the right one
|
||||
if (hasattr(obj_holidays, "PROVINCES") and
|
||||
if (hasattr(obj_holidays, 'PROVINCES') and
|
||||
province in obj_holidays.PROVINCES):
|
||||
obj_holidays = getattr(holidays, country)(prov=province,
|
||||
years=year)
|
||||
elif (hasattr(obj_holidays, "STATES") and
|
||||
obj_holidays = getattr(holidays, country)(
|
||||
prov=province, years=year)
|
||||
elif (hasattr(obj_holidays, 'STATES') and
|
||||
province in obj_holidays.STATES):
|
||||
obj_holidays = getattr(holidays, country)(state=province,
|
||||
years=year)
|
||||
obj_holidays = getattr(holidays, country)(
|
||||
state=province, years=year)
|
||||
else:
|
||||
_LOGGER.error("There is no province/state %s in country %s",
|
||||
province, country)
|
||||
|
||||
@@ -4,30 +4,29 @@ 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 logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD
|
||||
)
|
||||
|
||||
REQUIREMENTS = ['bimmer_connected==0.4.1']
|
||||
REQUIREMENTS = ['bimmer_connected==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'bmw_connected_drive'
|
||||
CONF_VALUES = 'values'
|
||||
CONF_COUNTRY = 'country'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
|
||||
ACCOUNT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_COUNTRY): cv.string,
|
||||
vol.Required(CONF_REGION): vol.Any('north_america', 'china',
|
||||
'rest_of_world'),
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@@ -37,7 +36,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
BMW_COMPONENTS = ['device_tracker', 'sensor']
|
||||
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
|
||||
UPDATE_INTERVAL = 5 # in minutes
|
||||
|
||||
|
||||
@@ -47,9 +46,9 @@ def setup(hass, config):
|
||||
for name, account_config in config[DOMAIN].items():
|
||||
username = account_config[CONF_USERNAME]
|
||||
password = account_config[CONF_PASSWORD]
|
||||
country = account_config[CONF_COUNTRY]
|
||||
region = account_config[CONF_REGION]
|
||||
_LOGGER.debug('Adding new account %s', name)
|
||||
bimmer = BMWConnectedDriveAccount(username, password, country, name)
|
||||
bimmer = BMWConnectedDriveAccount(username, password, region, name)
|
||||
accounts.append(bimmer)
|
||||
|
||||
# update every UPDATE_INTERVAL minutes, starting now
|
||||
@@ -75,12 +74,15 @@ def setup(hass, config):
|
||||
class BMWConnectedDriveAccount(object):
|
||||
"""Representation of a BMW vehicle."""
|
||||
|
||||
def __init__(self, username: str, password: str, country: str,
|
||||
def __init__(self, username: str, password: str, region_str: str,
|
||||
name: str) -> None:
|
||||
"""Constructor."""
|
||||
from bimmer_connected.account import ConnectedDriveAccount
|
||||
from bimmer_connected.country_selector import get_region_from_name
|
||||
|
||||
self.account = ConnectedDriveAccount(username, password, country)
|
||||
region = get_region_from_name(region_str)
|
||||
|
||||
self.account = ConnectedDriveAccount(username, password, region)
|
||||
self.name = name
|
||||
self._update_listeners = []
|
||||
|
||||
|
||||
@@ -194,7 +194,9 @@ class WebDavCalendarData(object):
|
||||
@staticmethod
|
||||
def is_over(vevent):
|
||||
"""Return if the event is over."""
|
||||
return dt.now() > WebDavCalendarData.get_end_date(vevent)
|
||||
return dt.now() >= WebDavCalendarData.to_datetime(
|
||||
WebDavCalendarData.get_end_date(vevent)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_hass_date(obj):
|
||||
@@ -230,4 +232,4 @@ class WebDavCalendarData(object):
|
||||
else:
|
||||
enddate = obj.dtstart.value + timedelta(days=1)
|
||||
|
||||
return WebDavCalendarData.to_datetime(enddate)
|
||||
return enddate
|
||||
|
||||
@@ -62,7 +62,14 @@ class GoogleCalendarData(object):
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
from httplib2 import ServerNotFoundError
|
||||
|
||||
try:
|
||||
service = self.calendar_service.get()
|
||||
except ServerNotFoundError:
|
||||
_LOGGER.warning("Unable to connect to Google, using cached data")
|
||||
return False
|
||||
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
|
||||
@@ -496,6 +496,10 @@ class TodoistProjectData(object):
|
||||
# We had no valid tasks
|
||||
return True
|
||||
|
||||
# Make sure the task collection is reset to prevent an
|
||||
# infinite collection repeating the same tasks
|
||||
self.all_project_tasks.clear()
|
||||
|
||||
# Organize the best tasks (so users can see all the tasks
|
||||
# they have, organized)
|
||||
while project_tasks:
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.components.camera import (
|
||||
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TOPIC = 'topic'
|
||||
|
||||
DEFAULT_NAME = 'MQTT Camera'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
@@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT Camera."""
|
||||
topic = config[CONF_TOPIC]
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
async_add_devices([MqttCamera(config[CONF_NAME], topic)])
|
||||
async_add_devices([MqttCamera(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_TOPIC)
|
||||
)])
|
||||
|
||||
|
||||
class MqttCamera(Camera):
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -103,92 +104,128 @@ class ONVIFHassCamera(Camera):
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a ONVIF camera."""
|
||||
from onvif import ONVIFCamera, exceptions
|
||||
super().__init__()
|
||||
import onvif
|
||||
|
||||
self._username = config.get(CONF_USERNAME)
|
||||
self._password = config.get(CONF_PASSWORD)
|
||||
self._host = config.get(CONF_HOST)
|
||||
self._port = config.get(CONF_PORT)
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
self._profile_index = config.get(CONF_PROFILE)
|
||||
self._input = None
|
||||
camera = None
|
||||
self._media_service = \
|
||||
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
|
||||
self._host, self._port),
|
||||
self._username, self._password,
|
||||
'{}/wsdl/media.wsdl'.format(os.path.dirname(
|
||||
onvif.__file__)))
|
||||
|
||||
self._ptz_service = \
|
||||
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
|
||||
self._host, self._port),
|
||||
self._username, self._password,
|
||||
'{}/wsdl/ptz.wsdl'.format(os.path.dirname(
|
||||
onvif.__file__)))
|
||||
|
||||
def obtain_input_uri(self):
|
||||
"""Set the input uri for the camera."""
|
||||
from onvif import exceptions
|
||||
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
|
||||
self._host, self._port)
|
||||
|
||||
try:
|
||||
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
|
||||
config.get(CONF_HOST), config.get(CONF_PORT))
|
||||
camera = ONVIFCamera(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)
|
||||
)
|
||||
media_service = camera.create_media_service()
|
||||
self._profiles = media_service.GetProfiles()
|
||||
self._profile_index = config.get(CONF_PROFILE)
|
||||
if self._profile_index >= len(self._profiles):
|
||||
profiles = self._media_service.GetProfiles()
|
||||
|
||||
if self._profile_index >= len(profiles):
|
||||
_LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d."
|
||||
" Using the last profile.",
|
||||
self._name, self._profile_index)
|
||||
self._profile_index = -1
|
||||
req = media_service.create_type('GetStreamUri')
|
||||
|
||||
req = self._media_service.create_type('GetStreamUri')
|
||||
|
||||
# pylint: disable=protected-access
|
||||
req.ProfileToken = self._profiles[self._profile_index]._token
|
||||
self._input = media_service.GetStreamUri(req).Uri.replace(
|
||||
'rtsp://', 'rtsp://{}:{}@'.format(
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD)), 1)
|
||||
req.ProfileToken = profiles[self._profile_index]._token
|
||||
uri_no_auth = self._media_service.GetStreamUri(req).Uri
|
||||
uri_for_log = uri_no_auth.replace(
|
||||
'rtsp://', 'rtsp://<user>:<password>@', 1)
|
||||
self._input = uri_no_auth.replace(
|
||||
'rtsp://', 'rtsp://{}:{}@'.format(self._username,
|
||||
self._password), 1)
|
||||
_LOGGER.debug(
|
||||
"ONVIF Camera Using the following URL for %s: %s",
|
||||
self._name, self._input)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Unable to communicate with ONVIF Camera: %s", err)
|
||||
raise
|
||||
try:
|
||||
self._ptz = camera.create_ptz_service()
|
||||
self._name, uri_for_log)
|
||||
# we won't need the media service anymore
|
||||
self._media_service = None
|
||||
except exceptions.ONVIFError as err:
|
||||
self._ptz = None
|
||||
_LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err)
|
||||
_LOGGER.debug("Couldn't setup camera '%s'. Error: %s",
|
||||
self._name, err)
|
||||
return
|
||||
|
||||
def perform_ptz(self, pan, tilt, zoom):
|
||||
"""Perform a PTZ action on the camera."""
|
||||
if self._ptz:
|
||||
from onvif import exceptions
|
||||
if self._ptz_service:
|
||||
pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0
|
||||
tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0
|
||||
zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
|
||||
req = {"Velocity": {
|
||||
"PanTilt": {"_x": pan_val, "_y": tilt_val},
|
||||
"Zoom": {"_x": zoom_val}}}
|
||||
self._ptz.ContinuousMove(req)
|
||||
try:
|
||||
self._ptz_service.ContinuousMove(req)
|
||||
except exceptions.ONVIFError as err:
|
||||
if "Bad Request" in err.reason:
|
||||
self._ptz_service = None
|
||||
_LOGGER.debug("Camera '%s' doesn't support PTZ.",
|
||||
self._name)
|
||||
else:
|
||||
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
if ONVIF_DATA not in self.hass.data:
|
||||
self.hass.data[ONVIF_DATA] = {}
|
||||
self.hass.data[ONVIF_DATA][ENTITIES] = []
|
||||
self.hass.data[ONVIF_DATA][ENTITIES].append(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
|
||||
if not self._input:
|
||||
await self.hass.async_add_job(self.obtain_input_uri)
|
||||
if not self._input:
|
||||
return None
|
||||
|
||||
ffmpeg = ImageFrame(
|
||||
self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
|
||||
|
||||
image = yield from asyncio.shield(ffmpeg.get_image(
|
||||
image = await asyncio.shield(ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
if not self._input:
|
||||
await self.hass.async_add_job(self.obtain_input_uri)
|
||||
if not self._input:
|
||||
return None
|
||||
|
||||
stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary,
|
||||
loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
await stream.open_camera(
|
||||
self._input, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -11,7 +11,7 @@ import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
async_add_devices([ProxyCamera(hass, config)])
|
||||
|
||||
|
||||
async def _read_frame(req):
|
||||
"""Read a single frame from an MJPEG stream."""
|
||||
# based on https://gist.github.com/russss/1143799
|
||||
import cgi
|
||||
# Read in HTTP headers:
|
||||
stream = req.content
|
||||
# multipart/x-mixed-replace; boundary=--frameboundary
|
||||
_mimetype, options = cgi.parse_header(req.headers['content-type'])
|
||||
boundary = options.get('boundary').encode('utf-8')
|
||||
if not boundary:
|
||||
_LOGGER.error("Malformed MJPEG missing boundary")
|
||||
raise Exception("Can't find content-type")
|
||||
|
||||
line = await stream.readline()
|
||||
# Seek ahead to the first chunk
|
||||
while line.strip() != boundary:
|
||||
line = await stream.readline()
|
||||
# Read in chunk headers
|
||||
while line.strip() != b'':
|
||||
parts = line.split(b':')
|
||||
if len(parts) > 1 and parts[0].lower() == b'content-length':
|
||||
# Grab chunk length
|
||||
length = int(parts[1].strip())
|
||||
line = await stream.readline()
|
||||
image = await stream.read(length)
|
||||
return image
|
||||
|
||||
|
||||
def _resize_image(image, opts):
|
||||
"""Resize image."""
|
||||
from PIL import Image
|
||||
@@ -227,9 +199,9 @@ class ProxyCamera(Camera):
|
||||
'boundary=--frameboundary')
|
||||
await response.prepare(request)
|
||||
|
||||
def write(img_bytes):
|
||||
async def write(img_bytes):
|
||||
"""Write image to stream."""
|
||||
response.write(bytes(
|
||||
await response.write(bytes(
|
||||
'--frameboundary\r\n'
|
||||
'Content-Type: {}\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
@@ -240,13 +212,23 @@ class ProxyCamera(Camera):
|
||||
req = await stream_coro
|
||||
|
||||
try:
|
||||
# This would be nicer as an async generator
|
||||
# But that would only be supported for python >=3.6
|
||||
data = b''
|
||||
stream = req.content
|
||||
while True:
|
||||
image = await _read_frame(req)
|
||||
if not image:
|
||||
chunk = await stream.read(102400)
|
||||
if not chunk:
|
||||
break
|
||||
image = await self.hass.async_add_job(
|
||||
_resize_image, image, self._stream_opts)
|
||||
write(image)
|
||||
data += chunk
|
||||
jpg_start = data.find(b'\xff\xd8')
|
||||
jpg_end = data.find(b'\xff\xd9')
|
||||
if jpg_start != -1 and jpg_end != -1:
|
||||
image = data[jpg_start:jpg_end + 2]
|
||||
image = await self.hass.async_add_job(
|
||||
_resize_image, image, self._stream_opts)
|
||||
await write(image)
|
||||
data = data[jpg_end + 2:]
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
req.close()
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Xeoma Cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.xeoma/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -14,7 +13,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyxeoma==1.3']
|
||||
REQUIREMENTS = ['pyxeoma==1.4.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,8 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Discover and setup Xeoma Cameras."""
|
||||
from pyxeoma.xeoma import Xeoma, XeomaError
|
||||
|
||||
@@ -53,8 +52,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
xeoma = Xeoma(host, login, password)
|
||||
|
||||
try:
|
||||
yield from xeoma.async_test_connection()
|
||||
discovered_image_names = yield from xeoma.async_get_image_names()
|
||||
await xeoma.async_test_connection()
|
||||
discovered_image_names = await xeoma.async_get_image_names()
|
||||
discovered_cameras = [
|
||||
{
|
||||
CONF_IMAGE_NAME: image_name,
|
||||
@@ -103,12 +102,11 @@ class XeomaCamera(Camera):
|
||||
self._password = password
|
||||
self._last_image = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from pyxeoma.xeoma import XeomaError
|
||||
try:
|
||||
image = yield from self._xeoma.async_get_camera_image(
|
||||
image = await self._xeoma.async_get_camera_image(
|
||||
self._image, self._username, self._password)
|
||||
self._last_image = image
|
||||
except XeomaError as err:
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['py-canary==0.4.1']
|
||||
REQUIREMENTS = ['py-canary==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
||||
ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_CONFIGURING = {}
|
||||
@@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
|
||||
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
|
||||
SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -122,6 +122,7 @@ class Thermostat(ClimateDevice):
|
||||
self._climate_list = self.climate_list
|
||||
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
|
||||
'heat', 'off']
|
||||
self._fan_list = ['auto', 'on']
|
||||
self.update_without_throttle = False
|
||||
|
||||
def update(self):
|
||||
@@ -180,24 +181,29 @@ class Thermostat(ClimateDevice):
|
||||
return self.thermostat['runtime']['desiredCool'] / 10.0
|
||||
return None
|
||||
|
||||
@property
|
||||
def desired_fan_mode(self):
|
||||
"""Return the desired fan mode of operation."""
|
||||
return self.thermostat['runtime']['desiredFanMode']
|
||||
|
||||
@property
|
||||
def fan(self):
|
||||
"""Return the current fan state."""
|
||||
"""Return the current fan status."""
|
||||
if 'fan' in self.thermostat['equipmentStatus']:
|
||||
return STATE_ON
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.thermostat['runtime']['desiredFanMode']
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
mode = self._current_hold_mode
|
||||
return None if mode == AWAY_MODE else mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
@property
|
||||
def _current_hold_mode(self):
|
||||
events = self.thermostat['events']
|
||||
@@ -206,7 +212,7 @@ class Thermostat(ClimateDevice):
|
||||
if event['type'] == 'hold':
|
||||
if event['holdClimateRef'] == 'away':
|
||||
if int(event['endDate'][0:4]) - \
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# A temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
# A permanent hold from away climate
|
||||
@@ -228,7 +234,7 @@ class Thermostat(ClimateDevice):
|
||||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
if self.operation_mode == 'auxHeatOnly' or \
|
||||
self.operation_mode == 'heatPump':
|
||||
self.operation_mode == 'heatPump':
|
||||
return STATE_HEAT
|
||||
return self.operation_mode
|
||||
|
||||
@@ -271,10 +277,11 @@ class Thermostat(ClimateDevice):
|
||||
operation = STATE_HEAT
|
||||
else:
|
||||
operation = status
|
||||
|
||||
return {
|
||||
"actual_humidity": self.thermostat['runtime']['actualHumidity'],
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"climate_mode": self.mode,
|
||||
"operation": operation,
|
||||
"climate_list": self.climate_list,
|
||||
"fan_min_on_time": self.fan_min_on_time
|
||||
@@ -342,25 +349,46 @@ class Thermostat(ClimateDevice):
|
||||
cool_temp_setpoint, heat_temp_setpoint,
|
||||
self.hold_preference())
|
||||
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
|
||||
"cool=%s, is=%s", heat_temp, isinstance(
|
||||
heat_temp, (int, float)), cool_temp,
|
||||
"cool=%s, is=%s", heat_temp,
|
||||
isinstance(heat_temp, (int, float)), cool_temp,
|
||||
isinstance(cool_temp, (int, float)))
|
||||
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
"""Set the fan mode. Valid values are "on" or "auto"."""
|
||||
if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO):
|
||||
error = "Invalid fan_mode value: Valid values are 'on' or 'auto'"
|
||||
_LOGGER.error(error)
|
||||
return
|
||||
|
||||
cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0
|
||||
heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0
|
||||
self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode,
|
||||
cool_temp, heat_temp,
|
||||
self.hold_preference())
|
||||
|
||||
_LOGGER.info("Setting fan mode to: %s", fan_mode)
|
||||
|
||||
def set_temp_hold(self, temp):
|
||||
"""Set temperature hold in modes other than auto."""
|
||||
# Set arbitrary range when not in auto mode
|
||||
if self.current_operation == STATE_HEAT:
|
||||
"""Set temperature hold in modes other than auto.
|
||||
|
||||
Ecobee API: It is good practice to set the heat and cool hold
|
||||
temperatures to be the same, if the thermostat is in either heat, cool,
|
||||
auxHeatOnly, or off mode. If the thermostat is in auto mode, an
|
||||
additional rule is required. The cool hold temperature must be greater
|
||||
than the heat hold temperature by at least the amount in the
|
||||
heatCoolMinDelta property.
|
||||
https://www.ecobee.com/home/developer/api/examples/ex5.shtml
|
||||
"""
|
||||
if self.current_operation == STATE_HEAT or self.current_operation == \
|
||||
STATE_COOL:
|
||||
heat_temp = temp
|
||||
cool_temp = temp + 20
|
||||
elif self.current_operation == STATE_COOL:
|
||||
heat_temp = temp - 20
|
||||
cool_temp = temp
|
||||
else:
|
||||
# In auto mode set temperature between
|
||||
heat_temp = temp - 10
|
||||
cool_temp = temp + 10
|
||||
delta = self.thermostat['settings']['heatCoolMinDelta'] / 10
|
||||
heat_temp = temp - delta
|
||||
cool_temp = temp + delta
|
||||
self.set_auto_temp_hold(heat_temp, cool_temp)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
@@ -369,8 +397,8 @@ class Thermostat(ClimateDevice):
|
||||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
if self.current_operation == STATE_AUTO and (low_temp is not None or
|
||||
high_temp is not None):
|
||||
if self.current_operation == STATE_AUTO and \
|
||||
(low_temp is not None or high_temp is not None):
|
||||
self.set_auto_temp_hold(low_temp, high_temp)
|
||||
elif temp is not None:
|
||||
self.set_temp_hold(temp)
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.9']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -31,10 +31,12 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the MySensors climate."""
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the mysensors climate."""
|
||||
mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices)
|
||||
hass, DOMAIN, discovery_info, MySensorsHVAC,
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
@@ -163,8 +165,8 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
self._values[self.value_type] = operation_mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
super().update()
|
||||
await super().async_update()
|
||||
self._values[self.value_type] = DICT_MYS_TO_HA[
|
||||
self._values[self.value_type]]
|
||||
|
||||
@@ -179,7 +179,7 @@ class NestThermostat(ClimateDevice):
|
||||
try:
|
||||
self.device.target = temp
|
||||
except nest.nest.APIError:
|
||||
_LOGGER.error("An error occured while setting the temperature")
|
||||
_LOGGER.error("An error occurred while setting the temperature")
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
|
||||
@@ -248,6 +248,11 @@ class SensiboClimate(ClimateDevice):
|
||||
return self._temperatures_list[-1] \
|
||||
if self._temperatures_list else super().max_temp
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID based on Sensibo ID."""
|
||||
return self._id
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
|
||||
@@ -37,6 +37,7 @@ CONF_FILTER = 'filter'
|
||||
CONF_GOOGLE_ACTIONS = 'google_actions'
|
||||
CONF_RELAYER = 'relayer'
|
||||
CONF_USER_POOL_ID = 'user_pool_id'
|
||||
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
|
||||
|
||||
DEFAULT_MODE = 'production'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -75,6 +76,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_USER_POOL_ID): str,
|
||||
vol.Optional(CONF_REGION): str,
|
||||
vol.Optional(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
}),
|
||||
@@ -110,7 +112,7 @@ class Cloud:
|
||||
|
||||
def __init__(self, hass, mode, alexa, google_actions,
|
||||
cognito_client_id=None, user_pool_id=None, region=None,
|
||||
relayer=None):
|
||||
relayer=None, google_actions_sync_url=None):
|
||||
"""Create an instance of Cloud."""
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
@@ -128,6 +130,7 @@ class Cloud:
|
||||
self.user_pool_id = user_pool_id
|
||||
self.region = region
|
||||
self.relayer = relayer
|
||||
self.google_actions_sync_url = google_actions_sync_url
|
||||
|
||||
else:
|
||||
info = SERVERS[mode]
|
||||
@@ -136,6 +139,7 @@ class Cloud:
|
||||
self.user_pool_id = info['user_pool_id']
|
||||
self.region = info['region']
|
||||
self.relayer = info['relayer']
|
||||
self.google_actions_sync_url = info['google_actions_sync_url']
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
|
||||
@@ -8,7 +8,9 @@ SERVERS = {
|
||||
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
|
||||
'user_pool_id': 'us-east-1_87ll5WOP8',
|
||||
'region': 'us-east-1',
|
||||
'relayer': 'wss://cloud.hass.io:8000/websocket'
|
||||
'relayer': 'wss://cloud.hass.io:8000/websocket',
|
||||
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
|
||||
'amazonaws.com/prod/smart_home_sync'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ from .const import DOMAIN, REQUEST_TIMEOUT
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
async def async_setup(hass):
|
||||
"""Initialize the HTTP API."""
|
||||
hass.http.register_view(GoogleActionsSyncView)
|
||||
hass.http.register_view(CloudLoginView)
|
||||
hass.http.register_view(CloudLogoutView)
|
||||
hass.http.register_view(CloudAccountView)
|
||||
@@ -38,12 +38,11 @@ _CLOUD_ERRORS = {
|
||||
|
||||
def _handle_cloud_errors(handler):
|
||||
"""Handle auth errors."""
|
||||
@asyncio.coroutine
|
||||
@wraps(handler)
|
||||
def error_handler(view, request, *args, **kwargs):
|
||||
async def error_handler(view, request, *args, **kwargs):
|
||||
"""Handle exceptions that raise from the wrapped request handler."""
|
||||
try:
|
||||
result = yield from handler(view, request, *args, **kwargs)
|
||||
result = await handler(view, request, *args, **kwargs)
|
||||
return result
|
||||
|
||||
except (auth_api.CloudError, asyncio.TimeoutError) as err:
|
||||
@@ -57,6 +56,31 @@ def _handle_cloud_errors(handler):
|
||||
return error_handler
|
||||
|
||||
|
||||
class GoogleActionsSyncView(HomeAssistantView):
|
||||
"""Trigger a Google Actions Smart Home Sync."""
|
||||
|
||||
url = '/api/cloud/google_actions/sync'
|
||||
name = 'api:cloud:google_actions/sync'
|
||||
|
||||
@_handle_cloud_errors
|
||||
async def post(self, request):
|
||||
"""Trigger a Google Actions sync."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
websession = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
await hass.async_add_job(auth_api.check_token, cloud)
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
req = await websession.post(
|
||||
cloud.google_actions_sync_url, headers={
|
||||
'authorization': cloud.id_token
|
||||
})
|
||||
|
||||
return self.json({}, status_code=req.status)
|
||||
|
||||
|
||||
class CloudLoginView(HomeAssistantView):
|
||||
"""Login to Home Assistant cloud."""
|
||||
|
||||
@@ -68,19 +92,18 @@ class CloudLoginView(HomeAssistantView):
|
||||
vol.Required('email'): str,
|
||||
vol.Required('password'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle login request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
|
||||
data['password'])
|
||||
await hass.async_add_job(auth_api.login, cloud, data['email'],
|
||||
data['password'])
|
||||
|
||||
hass.async_add_job(cloud.iot.connect)
|
||||
# Allow cloud to start connecting.
|
||||
yield from asyncio.sleep(0, loop=hass.loop)
|
||||
await asyncio.sleep(0, loop=hass.loop)
|
||||
return self.json(_account_data(cloud))
|
||||
|
||||
|
||||
@@ -91,14 +114,13 @@ class CloudLogoutView(HomeAssistantView):
|
||||
name = 'api:cloud:logout'
|
||||
|
||||
@_handle_cloud_errors
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
"""Handle logout request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from cloud.logout()
|
||||
await cloud.logout()
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
@@ -109,8 +131,7 @@ class CloudAccountView(HomeAssistantView):
|
||||
url = '/api/cloud/account'
|
||||
name = 'api:cloud:account'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
"""Get account info."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
@@ -132,14 +153,13 @@ class CloudRegisterView(HomeAssistantView):
|
||||
vol.Required('email'): str,
|
||||
vol.Required('password'): vol.All(str, vol.Length(min=6)),
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle registration request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
await hass.async_add_job(
|
||||
auth_api.register, cloud, data['email'], data['password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
@@ -155,14 +175,13 @@ class CloudResendConfirmView(HomeAssistantView):
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle resending confirm email code request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
await hass.async_add_job(
|
||||
auth_api.resend_email_confirm, cloud, data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
@@ -178,14 +197,13 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle forgot password request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
await hass.async_add_job(
|
||||
auth_api.forgot_password, cloud, data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
@@ -14,24 +14,16 @@ from homeassistant.util.yaml import load_yaml, dump
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
|
||||
'entity_registry')
|
||||
'entity_registry', 'config_entries')
|
||||
ON_DEMAND = ('zwave',)
|
||||
FEATURE_FLAGS = ('config_entries',)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""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."""
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
"""Example component to show how config entries work."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.util import slugify
|
||||
|
||||
|
||||
DOMAIN = 'config_entry_example'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup for our example component."""
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_entry(hass, entry):
|
||||
"""Initialize an entry."""
|
||||
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
|
||||
hass.states.async_set(entity_id, 'loaded', {
|
||||
ATTR_FRIENDLY_NAME: entry.data['name']
|
||||
})
|
||||
|
||||
# Indicate setup was successful.
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_unload_entry(hass, entry):
|
||||
"""Unload an entry."""
|
||||
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
|
||||
hass.states.async_remove(entity_id)
|
||||
|
||||
# Indicate unload was successful.
|
||||
return True
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class ExampleConfigFlow(config_entries.ConfigFlowHandler):
|
||||
"""Handle an example configuration flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a Hue config handler."""
|
||||
self.object_id = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_step_init(self, user_input=None):
|
||||
"""Start config flow."""
|
||||
errors = None
|
||||
if user_input is not None:
|
||||
object_id = user_input['object_id']
|
||||
|
||||
if object_id != '' and object_id == slugify(object_id):
|
||||
self.object_id = user_input['object_id']
|
||||
return (yield from self.async_step_name())
|
||||
|
||||
errors = {
|
||||
'object_id': 'Invalid object id.'
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
title='Pick object id',
|
||||
step_id='init',
|
||||
description="Please enter an object_id for the test entity.",
|
||||
data_schema=vol.Schema({
|
||||
'object_id': str
|
||||
}),
|
||||
errors=errors
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_step_name(self, user_input=None):
|
||||
"""Ask user to enter the name."""
|
||||
errors = None
|
||||
if user_input is not None:
|
||||
name = user_input['name']
|
||||
|
||||
if name != '':
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={
|
||||
'name': name,
|
||||
'object_id': self.object_id,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
title='Name of the entity',
|
||||
step_id='name',
|
||||
description="Please enter a name for the test entity.",
|
||||
data_schema=vol.Schema({
|
||||
'name': str
|
||||
}),
|
||||
errors=errors
|
||||
)
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_KEY_INSTANCE = 'configurator'
|
||||
|
||||
@@ -13,10 +13,14 @@ from homeassistant import core
|
||||
from homeassistant.components import http
|
||||
from homeassistant.components.http.data_validator import (
|
||||
RequestDataValidator)
|
||||
from homeassistant.components.cover import (INTENT_OPEN_COVER,
|
||||
INTENT_CLOSE_COVER)
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import intent
|
||||
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.setup import (ATTR_COMPONENT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,6 +32,13 @@ DOMAIN = 'conversation'
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
REGEX_TYPE = type(re.compile(''))
|
||||
|
||||
UTTERANCES = {
|
||||
'cover': {
|
||||
INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'],
|
||||
INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]']
|
||||
}
|
||||
}
|
||||
|
||||
SERVICE_PROCESS = 'process'
|
||||
|
||||
SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
@@ -112,6 +123,25 @@ async def async_setup(hass, config):
|
||||
'[the] [a] [an] {name}[s] toggle',
|
||||
])
|
||||
|
||||
@callback
|
||||
def register_utterances(component):
|
||||
"""Register utterances for a component."""
|
||||
if component not in UTTERANCES:
|
||||
return
|
||||
for intent_type, sentences in UTTERANCES[component].items():
|
||||
async_register(hass, intent_type, sentences)
|
||||
|
||||
@callback
|
||||
def component_loaded(event):
|
||||
"""Handle a new component loaded."""
|
||||
register_utterances(event.data[ATTR_COMPONENT])
|
||||
|
||||
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
|
||||
|
||||
# Check already loaded components.
|
||||
for component in hass.config.components:
|
||||
register_utterances(component)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import group
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.const import (
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
|
||||
@@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
|
||||
ATTR_POSITION = 'position'
|
||||
ATTR_TILT_POSITION = 'tilt_position'
|
||||
|
||||
INTENT_OPEN_COVER = 'HassOpenCover'
|
||||
INTENT_CLOSE_COVER = 'HassCloseCover'
|
||||
|
||||
COVER_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
@@ -181,6 +185,12 @@ async def async_setup(hass, config):
|
||||
hass.services.async_register(
|
||||
DOMAIN, service_name, async_handle_cover_service,
|
||||
schema=schema)
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER,
|
||||
"Opened {}"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER,
|
||||
"Closed {}"))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
120
homeassistant/components/cover/gogogate2.py
Normal file
120
homeassistant/components/cover/gogogate2.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Support for Gogogate2 Garage Doors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/cover.gogogate2/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN,
|
||||
CONF_IP_ADDRESS, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pygogogate2==0.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'gogogate2'
|
||||
|
||||
NOTIFICATION_ID = 'gogogate2_notification'
|
||||
NOTIFICATION_TITLE = 'Gogogate2 Cover Setup'
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_IP_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Gogogate2 component."""
|
||||
from pygogogate2 import Gogogate2API as pygogogate2
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
ip_address = config.get(CONF_IP_ADDRESS)
|
||||
name = config.get(CONF_NAME)
|
||||
mygogogate2 = pygogogate2(username, password, ip_address)
|
||||
|
||||
try:
|
||||
devices = mygogogate2.get_devices()
|
||||
if devices is False:
|
||||
raise ValueError(
|
||||
"Username or Password is incorrect or no devices found")
|
||||
|
||||
add_devices(MyGogogate2Device(
|
||||
mygogogate2, door, name) for door in devices)
|
||||
return
|
||||
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return
|
||||
|
||||
|
||||
class MyGogogate2Device(CoverDevice):
|
||||
"""Representation of a Gogogate2 cover."""
|
||||
|
||||
def __init__(self, mygogogate2, device, name):
|
||||
"""Initialize with API object, device id."""
|
||||
self.mygogogate2 = mygogogate2
|
||||
self.device_id = device['door']
|
||||
self._name = name or device['name']
|
||||
self._status = device['status']
|
||||
self.available = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door if any."""
|
||||
return self._name if self._name else DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if cover is closed, else False."""
|
||||
return self._status == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'garage'
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Could the device be accessed during the last update call."""
|
||||
return self.available
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Issue close command to cover."""
|
||||
self.mygogogate2.close_device(self.device_id)
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Issue open command to cover."""
|
||||
self.mygogogate2.open_device(self.device_id)
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def update(self):
|
||||
"""Update status of cover."""
|
||||
try:
|
||||
self._status = self.mygogogate2.get_status(self.device_id)
|
||||
self.available = True
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
self._status = STATE_UNKNOWN
|
||||
self.available = False
|
||||
271
homeassistant/components/cover/group.py
Executable file
271
homeassistant/components/cover/group.py
Executable file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
This platform allows several cover to be grouped into one cover.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.group/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN, PLATFORM_SCHEMA, CoverDevice, ATTR_POSITION,
|
||||
ATTR_CURRENT_POSITION, ATTR_TILT_POSITION, ATTR_CURRENT_TILT_POSITION,
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION,
|
||||
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT,
|
||||
SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION,
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
|
||||
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION)
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
|
||||
CONF_ENTITIES, CONF_NAME, STATE_CLOSED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
KEY_OPEN_CLOSE = 'open_close'
|
||||
KEY_STOP = 'stop'
|
||||
KEY_POSITION = 'position'
|
||||
|
||||
DEFAULT_NAME = 'Cover Group'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN),
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Group Cover platform."""
|
||||
async_add_devices(
|
||||
[CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])])
|
||||
|
||||
|
||||
class CoverGroup(CoverDevice):
|
||||
"""Representation of a CoverGroup."""
|
||||
|
||||
def __init__(self, name, entities):
|
||||
"""Initialize a CoverGroup entity."""
|
||||
self._name = name
|
||||
self._is_closed = False
|
||||
self._cover_position = 100
|
||||
self._tilt_position = None
|
||||
self._supported_features = 0
|
||||
self._assumed_state = True
|
||||
|
||||
self._entities = entities
|
||||
self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(),
|
||||
KEY_POSITION: set()}
|
||||
self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(),
|
||||
KEY_POSITION: set()}
|
||||
|
||||
@callback
|
||||
def update_supported_features(self, entity_id, old_state, new_state,
|
||||
update_state=True):
|
||||
"""Update dictionaries with supported features."""
|
||||
if not new_state:
|
||||
for values in self._covers.values():
|
||||
values.discard(entity_id)
|
||||
for values in self._tilts.values():
|
||||
values.discard(entity_id)
|
||||
if update_state:
|
||||
self.async_schedule_update_ha_state(True)
|
||||
return
|
||||
|
||||
features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if features & (SUPPORT_OPEN | SUPPORT_CLOSE):
|
||||
self._covers[KEY_OPEN_CLOSE].add(entity_id)
|
||||
else:
|
||||
self._covers[KEY_OPEN_CLOSE].discard(entity_id)
|
||||
if features & (SUPPORT_STOP):
|
||||
self._covers[KEY_STOP].add(entity_id)
|
||||
else:
|
||||
self._covers[KEY_STOP].discard(entity_id)
|
||||
if features & (SUPPORT_SET_POSITION):
|
||||
self._covers[KEY_POSITION].add(entity_id)
|
||||
else:
|
||||
self._covers[KEY_POSITION].discard(entity_id)
|
||||
|
||||
if features & (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT):
|
||||
self._tilts[KEY_OPEN_CLOSE].add(entity_id)
|
||||
else:
|
||||
self._tilts[KEY_OPEN_CLOSE].discard(entity_id)
|
||||
if features & (SUPPORT_STOP_TILT):
|
||||
self._tilts[KEY_STOP].add(entity_id)
|
||||
else:
|
||||
self._tilts[KEY_STOP].discard(entity_id)
|
||||
if features & (SUPPORT_SET_TILT_POSITION):
|
||||
self._tilts[KEY_POSITION].add(entity_id)
|
||||
else:
|
||||
self._tilts[KEY_POSITION].discard(entity_id)
|
||||
|
||||
if update_state:
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register listeners."""
|
||||
for entity_id in self._entities:
|
||||
new_state = self.hass.states.get(entity_id)
|
||||
self.update_supported_features(entity_id, None, new_state,
|
||||
update_state=False)
|
||||
async_track_state_change(self.hass, self._entities,
|
||||
self.update_supported_features)
|
||||
await self.async_update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Enable buttons even if at end position."""
|
||||
return self._assumed_state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling for cover group."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features for the cover."""
|
||||
return self._supported_features
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if all covers in group are closed."""
|
||||
return self._is_closed
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position for all covers."""
|
||||
return self._cover_position
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current tilt position for all covers."""
|
||||
return self._tilt_position
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Move the covers up."""
|
||||
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_OPEN_COVER, data, blocking=True)
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Move the covers down."""
|
||||
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True)
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_STOP_COVER, data, blocking=True)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Set covers position."""
|
||||
data = {ATTR_ENTITY_ID: self._covers[KEY_POSITION],
|
||||
ATTR_POSITION: kwargs[ATTR_POSITION]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True)
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs):
|
||||
"""Tilt covers open."""
|
||||
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True)
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs):
|
||||
"""Tilt covers closed."""
|
||||
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True)
|
||||
|
||||
async def async_stop_cover_tilt(self, **kwargs):
|
||||
"""Stop cover tilt."""
|
||||
data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True)
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Set tilt position."""
|
||||
data = {ATTR_ENTITY_ID: self._tilts[KEY_POSITION],
|
||||
ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION]}
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update state and attributes."""
|
||||
self._assumed_state = False
|
||||
|
||||
self._is_closed = True
|
||||
for entity_id in self._entities:
|
||||
state = self.hass.states.get(entity_id)
|
||||
if not state:
|
||||
continue
|
||||
if state.state != STATE_CLOSED:
|
||||
self._is_closed = False
|
||||
break
|
||||
|
||||
self._cover_position = None
|
||||
if self._covers[KEY_POSITION]:
|
||||
position = -1
|
||||
self._cover_position = 0 if self.is_closed else 100
|
||||
for entity_id in self._covers[KEY_POSITION]:
|
||||
state = self.hass.states.get(entity_id)
|
||||
pos = state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if position == -1:
|
||||
position = pos
|
||||
elif position != pos:
|
||||
self._assumed_state = True
|
||||
break
|
||||
else:
|
||||
if position != -1:
|
||||
self._cover_position = position
|
||||
|
||||
self._tilt_position = None
|
||||
if self._tilts[KEY_POSITION]:
|
||||
position = -1
|
||||
self._tilt_position = 100
|
||||
for entity_id in self._tilts[KEY_POSITION]:
|
||||
state = self.hass.states.get(entity_id)
|
||||
pos = state.attributes.get(ATTR_CURRENT_TILT_POSITION)
|
||||
if position == -1:
|
||||
position = pos
|
||||
elif position != pos:
|
||||
self._assumed_state = True
|
||||
break
|
||||
else:
|
||||
if position != -1:
|
||||
self._tilt_position = position
|
||||
|
||||
supported_features = 0
|
||||
supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE \
|
||||
if self._covers[KEY_OPEN_CLOSE] else 0
|
||||
supported_features |= SUPPORT_STOP \
|
||||
if self._covers[KEY_STOP] else 0
|
||||
supported_features |= SUPPORT_SET_POSITION \
|
||||
if self._covers[KEY_POSITION] else 0
|
||||
supported_features |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT \
|
||||
if self._tilts[KEY_OPEN_CLOSE] else 0
|
||||
supported_features |= SUPPORT_STOP_TILT \
|
||||
if self._tilts[KEY_STOP] else 0
|
||||
supported_features |= SUPPORT_SET_TILT_POSITION \
|
||||
if self._tilts[KEY_POSITION] else 0
|
||||
self._supported_features = supported_features
|
||||
|
||||
if not self._assumed_state:
|
||||
for entity_id in self._entities:
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state and state.attributes.get(ATTR_ASSUMED_STATE):
|
||||
self._assumed_state = True
|
||||
break
|
||||
@@ -9,10 +9,12 @@ from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the MySensors platform for covers."""
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the mysensors platform for covers."""
|
||||
mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices)
|
||||
hass, DOMAIN, discovery_info, MySensorsCover,
|
||||
async_add_devices=async_add_devices)
|
||||
|
||||
|
||||
class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
|
||||
@@ -87,7 +87,7 @@ class RPiGPIOCover(CoverDevice):
|
||||
self._invert_relay = invert_relay
|
||||
rpi_gpio.setup_output(self._relay_pin)
|
||||
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
|
||||
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
|
||||
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -105,9 +105,9 @@ class RPiGPIOCover(CoverDevice):
|
||||
|
||||
def _trigger(self):
|
||||
"""Trigger the cover."""
|
||||
rpi_gpio.write_output(self._relay_pin, self._invert_relay)
|
||||
rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0)
|
||||
sleep(self._relay_time)
|
||||
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
|
||||
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
|
||||
@@ -234,7 +234,9 @@ class CoverTemplate(CoverDevice):
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._position
|
||||
if self._position_template or self._position_script:
|
||||
return self._position
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
|
||||
25
homeassistant/components/deconz/.translations/en.json
Normal file
25
homeassistant/components/deconz/.translations/en.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "deCONZ",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Define deCONZ gateway",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (default value: '80')"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link with deCONZ",
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Couldn't get an API key"
|
||||
},
|
||||
"abort": {
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"one_instance_only": "Component only supports one deCONZ instance"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,21 @@ Support for deCONZ devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/deconz/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.discovery import SERVICE_DECONZ
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers import discovery, aiohttp_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['pydeconz==30']
|
||||
REQUIREMENTS = ['pydeconz==35']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,30 +56,28 @@ Unlock your deCONZ gateway to register with Home Assistant.
|
||||
"""
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up services and configuration for deCONZ component."""
|
||||
result = False
|
||||
config_file = yield from hass.async_add_job(
|
||||
config_file = await hass.async_add_job(
|
||||
load_json, hass.config.path(CONFIG_FILE))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_deconz_discovered(service, discovery_info):
|
||||
async def async_deconz_discovered(service, discovery_info):
|
||||
"""Call when deCONZ gateway has been found."""
|
||||
deconz_config = {}
|
||||
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
|
||||
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
|
||||
yield from async_request_configuration(hass, config, deconz_config)
|
||||
await async_request_configuration(hass, config, deconz_config)
|
||||
|
||||
if config_file:
|
||||
result = yield from async_setup_deconz(hass, config, config_file)
|
||||
result = await async_setup_deconz(hass, config, config_file)
|
||||
|
||||
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
|
||||
deconz_config = config[DOMAIN]
|
||||
if CONF_API_KEY in deconz_config:
|
||||
result = yield from async_setup_deconz(hass, config, deconz_config)
|
||||
result = await async_setup_deconz(hass, config, deconz_config)
|
||||
else:
|
||||
yield from async_request_configuration(hass, config, deconz_config)
|
||||
await async_request_configuration(hass, config, deconz_config)
|
||||
return True
|
||||
|
||||
if not result:
|
||||
@@ -89,8 +86,7 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_deconz(hass, config, deconz_config):
|
||||
async def async_setup_deconz(hass, config, deconz_config):
|
||||
"""Set up a deCONZ session.
|
||||
|
||||
Load config, group, light and sensor data for server information.
|
||||
@@ -100,7 +96,7 @@ def async_setup_deconz(hass, config, deconz_config):
|
||||
from pydeconz import DeconzSession
|
||||
websession = async_get_clientsession(hass)
|
||||
deconz = DeconzSession(hass.loop, websession, **deconz_config)
|
||||
result = yield from deconz.async_load_parameters()
|
||||
result = await deconz.async_load_parameters()
|
||||
if result is False:
|
||||
_LOGGER.error("Failed to communicate with deCONZ")
|
||||
return False
|
||||
@@ -113,8 +109,7 @@ def async_setup_deconz(hass, config, deconz_config):
|
||||
hass, component, DOMAIN, {}, config))
|
||||
deconz.start()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_configure(call):
|
||||
async def async_configure(call):
|
||||
"""Set attribute of device in deCONZ.
|
||||
|
||||
Field is a string representing a specific device in deCONZ
|
||||
@@ -140,7 +135,7 @@ def async_setup_deconz(hass, config, deconz_config):
|
||||
if field is None:
|
||||
_LOGGER.error('Could not find the entity %s', entity_id)
|
||||
return
|
||||
yield from deconz.async_put_state(field, data)
|
||||
await deconz.async_put_state(field, data)
|
||||
hass.services.async_register(
|
||||
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
|
||||
|
||||
@@ -159,21 +154,20 @@ def async_setup_deconz(hass, config, deconz_config):
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_request_configuration(hass, config, deconz_config):
|
||||
async def async_request_configuration(hass, config, deconz_config):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_configuration_callback(data):
|
||||
async def async_configuration_callback(data):
|
||||
"""Set up actions to do when our configuration callback is called."""
|
||||
from pydeconz.utils import async_get_api_key
|
||||
api_key = yield from async_get_api_key(hass.loop, **deconz_config)
|
||||
websession = async_get_clientsession(hass)
|
||||
api_key = await async_get_api_key(websession, **deconz_config)
|
||||
if api_key:
|
||||
deconz_config[CONF_API_KEY] = api_key
|
||||
result = yield from async_setup_deconz(hass, config, deconz_config)
|
||||
result = await async_setup_deconz(hass, config, deconz_config)
|
||||
if result:
|
||||
yield from hass.async_add_job(
|
||||
await hass.async_add_job(
|
||||
save_json, hass.config.path(CONFIG_FILE), deconz_config)
|
||||
configurator.async_request_done(request_id)
|
||||
return
|
||||
@@ -194,3 +188,85 @@ def async_request_configuration(hass, config, deconz_config):
|
||||
entity_picture="/static/images/logo_deconz.jpeg",
|
||||
submit_caption="I have unlocked the gateway",
|
||||
)
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class DeconzFlowHandler(config_entries.ConfigFlowHandler):
|
||||
"""Handle a deCONZ config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the deCONZ flow."""
|
||||
self.bridges = []
|
||||
self.deconz_config = {}
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
from pydeconz.utils import async_discovery
|
||||
|
||||
if DOMAIN in self.hass.data:
|
||||
return self.async_abort(
|
||||
reason='one_instance_only'
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
for bridge in self.bridges:
|
||||
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
||||
self.deconz_config = bridge
|
||||
return await self.async_step_link()
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
self.bridges = await async_discovery(session)
|
||||
|
||||
if len(self.bridges) == 1:
|
||||
self.deconz_config = self.bridges[0]
|
||||
return await self.async_step_link()
|
||||
elif len(self.bridges) > 1:
|
||||
hosts = []
|
||||
for bridge in self.bridges:
|
||||
hosts.append(bridge[CONF_HOST])
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_HOST): vol.In(hosts)
|
||||
})
|
||||
)
|
||||
|
||||
return self.async_abort(
|
||||
reason='no_bridges'
|
||||
)
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Attempt to link with the deCONZ bridge."""
|
||||
from pydeconz.utils import async_get_api_key
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
api_key = await async_get_api_key(session, **self.deconz_config)
|
||||
if api_key:
|
||||
self.deconz_config[CONF_API_KEY] = api_key
|
||||
return self.async_create_entry(
|
||||
title='deCONZ',
|
||||
data=self.deconz_config
|
||||
)
|
||||
else:
|
||||
errors['base'] = 'no_key'
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='link',
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up a bridge for a config entry."""
|
||||
if DOMAIN in hass.data:
|
||||
_LOGGER.error(
|
||||
"Config entry failed since one deCONZ instance already exists")
|
||||
return False
|
||||
result = await async_setup_deconz(hass, None, entry.data)
|
||||
if result:
|
||||
return True
|
||||
return False
|
||||
|
||||
25
homeassistant/components/deconz/strings.json
Normal file
25
homeassistant/components/deconz/strings.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "deCONZ",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Define deCONZ gateway",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port (default value: '80')"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link with deCONZ",
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"no_key": "Couldn't get an API key"
|
||||
},
|
||||
"abort": {
|
||||
"no_bridges": "No deCONZ bridges discovered",
|
||||
"one_instance_only": "Component only supports one deCONZ instance"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.yaml import dump
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PUB_KEY = 'pub_key'
|
||||
CONF_SSH_KEY = 'ssh_key'
|
||||
CONF_REQUIRE_IP = 'require_ip'
|
||||
DEFAULT_SSH_PORT = 22
|
||||
SECRET_GROUP = 'Password or SSH Key'
|
||||
|
||||
@@ -36,6 +37,7 @@ PLATFORM_SCHEMA = vol.All(
|
||||
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
|
||||
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
|
||||
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
|
||||
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
|
||||
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
|
||||
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
|
||||
@@ -115,6 +117,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
self.protocol = config[CONF_PROTOCOL]
|
||||
self.mode = config[CONF_MODE]
|
||||
self.port = config[CONF_PORT]
|
||||
self.require_ip = config[CONF_REQUIRE_IP]
|
||||
|
||||
if self.protocol == 'ssh':
|
||||
self.connection = SshConnection(
|
||||
@@ -172,7 +175,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
|
||||
ret_devices = {}
|
||||
for key in devices:
|
||||
if devices[key].ip is not None:
|
||||
if not self.require_ip or devices[key].ip is not None:
|
||||
ret_devices[key] = devices[key]
|
||||
return ret_devices
|
||||
|
||||
|
||||
@@ -17,12 +17,15 @@ import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pybluez==0.22']
|
||||
REQUIREMENTS = ['pybluez==0.22', 'bt_proximity==0.1.2']
|
||||
|
||||
BT_PREFIX = 'BT_'
|
||||
|
||||
CONF_REQUEST_RSSI = 'request_rssi'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_TRACK_NEW): cv.boolean
|
||||
vol.Optional(CONF_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_REQUEST_RSSI): cv.boolean
|
||||
})
|
||||
|
||||
|
||||
@@ -30,11 +33,15 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up the Bluetooth Scanner."""
|
||||
# pylint: disable=import-error
|
||||
import bluetooth
|
||||
from bt_proximity import BluetoothRSSI
|
||||
|
||||
def see_device(device):
|
||||
def see_device(mac, name, rssi=None):
|
||||
"""Mark a device as seen."""
|
||||
see(mac=BT_PREFIX + device[0], host_name=device[1],
|
||||
source_type=SOURCE_TYPE_BLUETOOTH)
|
||||
attributes = {}
|
||||
if rssi is not None:
|
||||
attributes['rssi'] = rssi
|
||||
see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name,
|
||||
attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH)
|
||||
|
||||
def discover_devices():
|
||||
"""Discover Bluetooth devices."""
|
||||
@@ -64,27 +71,32 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
if track_new:
|
||||
for dev in discover_devices():
|
||||
if dev[0] not in devs_to_track and \
|
||||
dev[0] not in devs_donot_track:
|
||||
dev[0] not in devs_donot_track:
|
||||
devs_to_track.append(dev[0])
|
||||
see_device(dev)
|
||||
see_device(dev[0], dev[1])
|
||||
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
request_rssi = config.get(CONF_REQUEST_RSSI, False)
|
||||
|
||||
def update_bluetooth(now):
|
||||
"""Lookup Bluetooth device and update status."""
|
||||
try:
|
||||
if track_new:
|
||||
for dev in discover_devices():
|
||||
if dev[0] not in devs_to_track and \
|
||||
dev[0] not in devs_donot_track:
|
||||
dev[0] not in devs_donot_track:
|
||||
devs_to_track.append(dev[0])
|
||||
for mac in devs_to_track:
|
||||
_LOGGER.debug("Scanning %s", mac)
|
||||
result = bluetooth.lookup_name(mac, timeout=5)
|
||||
if not result:
|
||||
rssi = None
|
||||
if request_rssi:
|
||||
rssi = BluetoothRSSI(mac).request_rssi()
|
||||
if result is None:
|
||||
# Could not lookup device name
|
||||
continue
|
||||
see_device((mac, result))
|
||||
see_device(mac, result, rssi)
|
||||
except bluetooth.BluetoothError:
|
||||
_LOGGER.exception("Error looking up Bluetooth device")
|
||||
track_point_in_utc_time(
|
||||
|
||||
@@ -36,16 +36,20 @@ class BMWDeviceTracker(object):
|
||||
self.vehicle = vehicle
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the device info."""
|
||||
dev_id = slugify(self.vehicle.modelName)
|
||||
"""Update the device info.
|
||||
|
||||
Only update the state in home assistant if tracking in
|
||||
the car is enabled.
|
||||
"""
|
||||
dev_id = slugify(self.vehicle.name)
|
||||
|
||||
if not self.vehicle.state.is_vehicle_tracking_enabled:
|
||||
_LOGGER.debug('Tracking is disabled for vehicle %s', dev_id)
|
||||
return
|
||||
|
||||
_LOGGER.debug('Updating %s', dev_id)
|
||||
attrs = {
|
||||
'trackr_id': dev_id,
|
||||
'id': dev_id,
|
||||
'name': self.vehicle.modelName
|
||||
}
|
||||
|
||||
self._see(
|
||||
dev_id=dev_id, host_name=self.vehicle.modelName,
|
||||
gps=self.vehicle.state.gps_position, attributes=attrs,
|
||||
icon='mdi:car'
|
||||
dev_id=dev_id, host_name=self.vehicle.name,
|
||||
gps=self.vehicle.state.gps_position, icon='mdi:car'
|
||||
)
|
||||
|
||||
83
homeassistant/components/device_tracker/google_maps.py
Normal file
83
homeassistant/components/device_tracker/google_maps.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Support for Google Maps location sharing.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.google_maps/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, SOURCE_TYPE_GPS)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==0.4.0']
|
||||
|
||||
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: ConfigType, see, discovery_info=None):
|
||||
"""Set up the scanner."""
|
||||
scanner = GoogleMapsScanner(hass, config, see)
|
||||
return scanner.success_init
|
||||
|
||||
|
||||
class GoogleMapsScanner(object):
|
||||
"""Representation of an Google Maps location sharing account."""
|
||||
|
||||
def __init__(self, hass, config: ConfigType, see) -> None:
|
||||
"""Initialize the scanner."""
|
||||
from locationsharinglib import Service
|
||||
from locationsharinglib.locationsharinglibexceptions import InvalidUser
|
||||
|
||||
self.see = see
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
self.service = Service(self.username, self.password,
|
||||
hass.config.path(CREDENTIALS_FILE))
|
||||
self._update_info()
|
||||
|
||||
track_time_interval(
|
||||
hass, self._update_info, MIN_TIME_BETWEEN_SCANS)
|
||||
|
||||
self.success_init = True
|
||||
|
||||
except InvalidUser:
|
||||
_LOGGER.error('You have specified invalid login credentials')
|
||||
self.success_init = False
|
||||
|
||||
def _update_info(self, now=None):
|
||||
for person in self.service.get_all_people():
|
||||
dev_id = 'google_maps_{0}'.format(slugify(person.id))
|
||||
|
||||
attrs = {
|
||||
'id': person.id,
|
||||
'nickname': person.nickname,
|
||||
'full_name': person.full_name,
|
||||
'last_seen': person.datetime,
|
||||
'address': person.address
|
||||
}
|
||||
self.see(
|
||||
dev_id=dev_id,
|
||||
gps=(person.latitude, person.longitude),
|
||||
picture=person.picture_url,
|
||||
source_type=SOURCE_TYPE_GPS,
|
||||
attributes=attrs
|
||||
)
|
||||
@@ -1,74 +0,0 @@
|
||||
"""
|
||||
Support for Mercedes cars with Mercedes ME.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.mercedesme/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.mercedesme import DATA_MME
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['mercedesme']
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up the Mercedes ME tracker."""
|
||||
if discovery_info is None:
|
||||
return False
|
||||
|
||||
data = hass.data[DATA_MME].data
|
||||
|
||||
if not data.cars:
|
||||
return False
|
||||
|
||||
MercedesMEDeviceTracker(hass, config, see, data)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MercedesMEDeviceTracker(object):
|
||||
"""A class representing a Mercedes ME device tracker."""
|
||||
|
||||
def __init__(self, hass, config, see, data):
|
||||
"""Initialize the Mercedes ME device tracker."""
|
||||
self.see = see
|
||||
self.data = data
|
||||
self.update_info()
|
||||
|
||||
track_time_interval(
|
||||
hass, self.update_info, MIN_TIME_BETWEEN_SCANS)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def update_info(self, now=None):
|
||||
"""Update the device info."""
|
||||
for device in self.data.cars:
|
||||
if not device['services'].get('VEHICLE_FINDER', False):
|
||||
continue
|
||||
|
||||
location = self.data.get_location(device["vin"])
|
||||
if location is None:
|
||||
continue
|
||||
|
||||
dev_id = device["vin"]
|
||||
name = device["license"]
|
||||
|
||||
lat = location['positionLat']['value']
|
||||
lon = location['positionLong']['value']
|
||||
attrs = {
|
||||
'trackr_id': dev_id,
|
||||
'id': dev_id,
|
||||
'name': name
|
||||
}
|
||||
self.see(
|
||||
dev_id=dev_id, host_name=name,
|
||||
gps=(lat, lon), attributes=attrs
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner):
|
||||
self.host,
|
||||
self.username,
|
||||
self.password,
|
||||
port=int(self.port)
|
||||
port=int(self.port),
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -6,15 +6,15 @@ https://home-assistant.io/components/device_tracker.mysensors/
|
||||
"""
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util import slugify
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up the MySensors device scanner."""
|
||||
new_devices = mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
|
||||
device_args=(see, ))
|
||||
device_args=(async_see, ))
|
||||
if not new_devices:
|
||||
return False
|
||||
|
||||
@@ -22,9 +22,9 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
dev_id = (
|
||||
id(device.gateway), device.node_id, device.child_id,
|
||||
device.value_type)
|
||||
dispatcher_connect(
|
||||
async_dispatcher_connect(
|
||||
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
|
||||
device.update_callback)
|
||||
device.async_update_callback)
|
||||
|
||||
return True
|
||||
|
||||
@@ -32,20 +32,20 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
class MySensorsDeviceScanner(mysensors.MySensorsDevice):
|
||||
"""Represent a MySensors scanner."""
|
||||
|
||||
def __init__(self, see, *args):
|
||||
def __init__(self, async_see, *args):
|
||||
"""Set up instance."""
|
||||
super().__init__(*args)
|
||||
self.see = see
|
||||
self.async_see = async_see
|
||||
|
||||
def update_callback(self):
|
||||
async def async_update_callback(self):
|
||||
"""Update the device."""
|
||||
self.update()
|
||||
await self.async_update()
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
child = node.children[self.child_id]
|
||||
position = child.values[self.value_type]
|
||||
latitude, longitude, _ = position.split(',')
|
||||
|
||||
self.see(
|
||||
await self.async_see(
|
||||
dev_id=slugify(self.name),
|
||||
host_name=self.name,
|
||||
gps=(latitude, longitude),
|
||||
|
||||
@@ -100,7 +100,7 @@ class TadoDeviceScanner(DeviceScanner):
|
||||
last_results = []
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
with async_timeout.timeout(10):
|
||||
# Format the URL here, so we can log the template URL if
|
||||
# anything goes wrong without exposing username and password.
|
||||
url = self.tadoapiurl.format(
|
||||
|
||||
@@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner):
|
||||
return self.last_results
|
||||
|
||||
def _generate_mac2name(self):
|
||||
"""Return empty MAC to name dict. Overriden if DHCP server is set."""
|
||||
"""Return empty MAC to name dict. Overridden if DHCP server is set."""
|
||||
self.mac2name = dict()
|
||||
|
||||
@_refresh_on_access_denied
|
||||
|
||||
@@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner):
|
||||
# Filter clients to provided SSID list
|
||||
if self._ssid_filter:
|
||||
clients = [client for client in clients
|
||||
if client['essid'] in self._ssid_filter]
|
||||
if 'essid' in client and
|
||||
client['essid'] in self._ssid_filter]
|
||||
|
||||
self._clients = {
|
||||
client['mac']: client
|
||||
|
||||
77
homeassistant/components/device_tracker/xiaomi_miio.py
Normal file
77
homeassistant/components/device_tracker/xiaomi_miio.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Support for Xiaomi Mi WiFi Repeater 2.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/device_tracker.xiaomi_miio/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
|
||||
DeviceScanner)
|
||||
from homeassistant.const import (CONF_HOST, CONF_TOKEN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.9']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Return a Xiaomi MiIO device scanner."""
|
||||
from miio import WifiRepeater, DeviceException
|
||||
|
||||
scanner = None
|
||||
host = config[DOMAIN].get(CONF_HOST)
|
||||
token = config[DOMAIN].get(CONF_TOKEN)
|
||||
|
||||
_LOGGER.info(
|
||||
"Initializing with host %s (token %s...)", host, token[:5])
|
||||
|
||||
try:
|
||||
device = WifiRepeater(host, token)
|
||||
device_info = device.info()
|
||||
_LOGGER.info("%s %s %s detected",
|
||||
device_info.model,
|
||||
device_info.firmware_version,
|
||||
device_info.hardware_version)
|
||||
scanner = XiaomiMiioDeviceScanner(hass, device)
|
||||
except DeviceException as ex:
|
||||
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
|
||||
|
||||
return scanner
|
||||
|
||||
|
||||
class XiaomiMiioDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Xiaomi Mi WiFi Repeater."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize the scanner."""
|
||||
self.device = device
|
||||
|
||||
async def async_scan_devices(self):
|
||||
"""Scan for devices and return a list containing found device ids."""
|
||||
from miio import DeviceException
|
||||
|
||||
devices = []
|
||||
try:
|
||||
station_info = await self.hass.async_add_job(self.device.status)
|
||||
_LOGGER.debug("Got new station info: %s", station_info)
|
||||
|
||||
for device in station_info['mat']:
|
||||
devices.append(device['mac'])
|
||||
|
||||
except DeviceException as ex:
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return devices
|
||||
|
||||
async def async_get_device_name(self, device):
|
||||
"""The repeater doesn't provide the name of the associated device."""
|
||||
return None
|
||||
@@ -6,7 +6,6 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
|
||||
Knows which components handle certain types, will make sure they are
|
||||
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
@@ -14,6 +13,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==1.2.4']
|
||||
REQUIREMENTS = ['netdisco==1.3.0']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
@@ -39,6 +39,11 @@ SERVICE_TELLDUSLIVE = 'tellstick'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_DECONZ = 'deconz'
|
||||
SERVICE_DAIKIN = 'daikin'
|
||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||
|
||||
CONFIG_ENTRY_HANDLERS = {
|
||||
SERVICE_HUE: 'hue',
|
||||
}
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
@@ -51,9 +56,9 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_WINK: ('wink', None),
|
||||
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
||||
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
||||
SERVICE_HUE: ('hue', None),
|
||||
SERVICE_DECONZ: ('deconz', None),
|
||||
SERVICE_DAIKIN: ('daikin', None),
|
||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
@@ -84,8 +89,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Start a discovery service."""
|
||||
from netdisco.discovery import NetworkDiscovery
|
||||
|
||||
@@ -99,13 +103,26 @@ def async_setup(hass, config):
|
||||
# Platforms ignore by config
|
||||
ignored_platforms = config[DOMAIN][CONF_IGNORE]
|
||||
|
||||
@asyncio.coroutine
|
||||
def new_service_found(service, info):
|
||||
async def new_service_found(service, info):
|
||||
"""Handle a new service if one is found."""
|
||||
if service in ignored_platforms:
|
||||
logger.info("Ignoring service: %s %s", service, info)
|
||||
return
|
||||
|
||||
discovery_hash = json.dumps([service, info], sort_keys=True)
|
||||
if discovery_hash in already_discovered:
|
||||
return
|
||||
|
||||
already_discovered.add(discovery_hash)
|
||||
|
||||
if service in CONFIG_ENTRY_HANDLERS:
|
||||
await hass.config_entries.flow.async_init(
|
||||
CONFIG_ENTRY_HANDLERS[service],
|
||||
source=config_entries.SOURCE_DISCOVERY,
|
||||
data=info
|
||||
)
|
||||
return
|
||||
|
||||
comp_plat = SERVICE_HANDLERS.get(service)
|
||||
|
||||
# We do not know how to handle this service.
|
||||
@@ -113,26 +130,19 @@ def async_setup(hass, config):
|
||||
logger.info("Unknown service discovered: %s %s", service, info)
|
||||
return
|
||||
|
||||
discovery_hash = json.dumps([service, info], sort_keys=True)
|
||||
if discovery_hash in already_discovered:
|
||||
return
|
||||
|
||||
already_discovered.add(discovery_hash)
|
||||
|
||||
logger.info("Found new service: %s %s", service, info)
|
||||
|
||||
component, platform = comp_plat
|
||||
|
||||
if platform is None:
|
||||
yield from async_discover(hass, service, info, component, config)
|
||||
await async_discover(hass, service, info, component, config)
|
||||
else:
|
||||
yield from async_load_platform(
|
||||
await async_load_platform(
|
||||
hass, component, platform, info, config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def scan_devices(now):
|
||||
async def scan_devices(now):
|
||||
"""Scan for devices."""
|
||||
results = yield from hass.async_add_job(_discover, netdisco)
|
||||
results = await hass.async_add_job(_discover, netdisco)
|
||||
|
||||
for result in results:
|
||||
hass.async_add_job(new_service_found(*result))
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['DoorBirdPy==0.1.2']
|
||||
REQUIREMENTS = ['DoorBirdPy==0.1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,6 +22,7 @@ DOMAIN = 'doorbird'
|
||||
API_URL = '/api/{}'.format(DOMAIN)
|
||||
|
||||
CONF_DOORBELL_EVENTS = 'doorbell_events'
|
||||
CONF_CUSTOM_URL = 'hass_url_override'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
|
||||
vol.Optional(CONF_CUSTOM_URL): cv.string,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -61,9 +63,17 @@ def setup(hass, config):
|
||||
# Provide an endpoint for the device to call to trigger events
|
||||
hass.http.register_view(DoorbirdRequestView())
|
||||
|
||||
# Get the URL of this server
|
||||
hass_url = hass.config.api.base_url
|
||||
|
||||
# Override it if another is specified in the component configuration
|
||||
if config[DOMAIN].get(CONF_CUSTOM_URL):
|
||||
hass_url = config[DOMAIN].get(CONF_CUSTOM_URL)
|
||||
_LOGGER.info("DoorBird will connect to this instance via %s",
|
||||
hass_url)
|
||||
|
||||
# This will make HA the only service that gets doorbell events
|
||||
url = '{}{}/{}'.format(
|
||||
hass.config.api.base_url, API_URL, SENSOR_DOORBELL)
|
||||
url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
|
||||
device.reset_notifications()
|
||||
device.subscribe_notification(SENSOR_DOORBELL, url)
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ ATTR_OVERWRITE = 'overwrite'
|
||||
CONF_DOWNLOAD_DIR = 'download_dir'
|
||||
|
||||
DOMAIN = 'downloader'
|
||||
DOWNLOAD_FAILED_EVENT = 'download_failed'
|
||||
DOWNLOAD_COMPLETED_EVENT = 'download_completed'
|
||||
|
||||
SERVICE_DOWNLOAD_FILE = 'download_file'
|
||||
|
||||
@@ -133,9 +135,19 @@ def setup(hass, config):
|
||||
fil.write(chunk)
|
||||
|
||||
_LOGGER.debug("Downloading of %s done", url)
|
||||
hass.bus.fire(
|
||||
"{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), {
|
||||
'url': url,
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.exception("ConnectionError occurred for %s", url)
|
||||
hass.bus.fire(
|
||||
"{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), {
|
||||
'url': url,
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
# Remove file if we started downloading but failed
|
||||
if final_path and os.path.isfile(final_path):
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.util.json import save_json
|
||||
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.15']
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.17']
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.38']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.39']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -158,10 +158,6 @@ class Config(object):
|
||||
"Listen port not specified, defaulting to %s",
|
||||
self.listen_port)
|
||||
|
||||
if self.type == TYPE_GOOGLE and self.listen_port != 80:
|
||||
_LOGGER.warning("When targeting Google Home, listening port has "
|
||||
"to be port 80")
|
||||
|
||||
# Get whether or not UPNP binds to multicast address (239.255.255.250)
|
||||
# or to the unicast address (host_ip_addr)
|
||||
self.upnp_bind_multicast = conf.get(
|
||||
|
||||
@@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on:
|
||||
description: Turn the buzzer on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_buzzer_off:
|
||||
description: Turn the buzzer off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_led_on:
|
||||
description: Turn the led on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_led_off:
|
||||
description: Turn the led off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_child_lock_on:
|
||||
description: Turn the child lock on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_child_lock_off:
|
||||
description: Turn the child lock off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_favorite_level:
|
||||
description: Set the favorite level.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
level:
|
||||
description: Level, between 0 and 16.
|
||||
example: 1
|
||||
@@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness:
|
||||
description: Set the led brightness.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the air purifier entity.
|
||||
example: 'fan.xiaomi_air_purifier'
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
brightness:
|
||||
description: Brightness (0 = Bright, 1 = Dim, 2 = Off)
|
||||
example: 1
|
||||
|
||||
xiaomi_miio_set_auto_detect_on:
|
||||
description: Turn the auto detect on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_auto_detect_off:
|
||||
description: Turn the auto detect off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_learn_mode_on:
|
||||
description: Turn the learn mode on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_learn_mode_off:
|
||||
description: Turn the learn mode off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_volume:
|
||||
description: Set the sound volume.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
volume:
|
||||
description: Volume, between 0 and 100.
|
||||
example: 50
|
||||
|
||||
xiaomi_miio_reset_filter:
|
||||
description: Reset the filter lifetime and usage.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_extra_features:
|
||||
description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
features:
|
||||
description: Integer, known values are 0 (default) and 1 (turbo mode).
|
||||
example: 1
|
||||
|
||||
xiaomi_miio_set_target_humidity:
|
||||
description: Set the target humidity.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
humidity:
|
||||
description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80.
|
||||
example: 50
|
||||
|
||||
xiaomi_miio_set_dry_on:
|
||||
description: Turn the dry mode on.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
xiaomi_miio_set_dry_off:
|
||||
description: Turn the dry mode off.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the xiaomi miio entity.
|
||||
example: 'fan.xiaomi_miio_device'
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""
|
||||
Support for Xiaomi Mi Air Purifier 2.
|
||||
Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/fan.xiaomi_miio/
|
||||
"""
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
|
||||
SUPPORT_SET_SPEED, DOMAIN, )
|
||||
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
|
||||
@@ -20,17 +20,40 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Xiaomi Air Purifier'
|
||||
PLATFORM = 'xiaomi_miio'
|
||||
DEFAULT_NAME = 'Xiaomi Miio Device'
|
||||
DATA_KEY = 'fan.xiaomi_miio'
|
||||
|
||||
CONF_MODEL = 'model'
|
||||
MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6'
|
||||
MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3'
|
||||
MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1'
|
||||
MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_MODEL): vol.In(
|
||||
['zhimi.airpurifier.m1',
|
||||
'zhimi.airpurifier.m2',
|
||||
'zhimi.airpurifier.ma1',
|
||||
'zhimi.airpurifier.ma2',
|
||||
'zhimi.airpurifier.sa1',
|
||||
'zhimi.airpurifier.sa2',
|
||||
'zhimi.airpurifier.v1',
|
||||
'zhimi.airpurifier.v2',
|
||||
'zhimi.airpurifier.v3',
|
||||
'zhimi.airpurifier.v5',
|
||||
'zhimi.airpurifier.v6',
|
||||
'zhimi.humidifier.v1',
|
||||
'zhimi.humidifier.ca1']),
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.7']
|
||||
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
|
||||
|
||||
ATTR_MODEL = 'model'
|
||||
|
||||
# Air Purifier
|
||||
ATTR_TEMPERATURE = 'temperature'
|
||||
ATTR_HUMIDITY = 'humidity'
|
||||
ATTR_AIR_QUALITY_INDEX = 'aqi'
|
||||
@@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness'
|
||||
ATTR_MOTOR_SPEED = 'motor_speed'
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi'
|
||||
ATTR_PURIFY_VOLUME = 'purify_volume'
|
||||
|
||||
ATTR_BRIGHTNESS = 'brightness'
|
||||
ATTR_LEVEL = 'level'
|
||||
ATTR_MOTOR2_SPEED = 'motor2_speed'
|
||||
ATTR_ILLUMINANCE = 'illuminance'
|
||||
ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id'
|
||||
ATTR_FILTER_RFID_TAG = 'filter_rfid_tag'
|
||||
ATTR_FILTER_TYPE = 'filter_type'
|
||||
ATTR_LEARN_MODE = 'learn_mode'
|
||||
ATTR_SLEEP_TIME = 'sleep_time'
|
||||
ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count'
|
||||
ATTR_EXTRA_FEATURES = 'extra_features'
|
||||
ATTR_FEATURES = 'features'
|
||||
ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported'
|
||||
ATTR_AUTO_DETECT = 'auto_detect'
|
||||
ATTR_SLEEP_MODE = 'sleep_mode'
|
||||
ATTR_VOLUME = 'volume'
|
||||
ATTR_USE_TIME = 'use_time'
|
||||
ATTR_BUTTON_PRESSED = 'button_pressed'
|
||||
|
||||
# Air Humidifier
|
||||
ATTR_TARGET_HUMIDITY = 'target_humidity'
|
||||
ATTR_TRANS_LEVEL = 'trans_level'
|
||||
ATTR_HARDWARE_VERSION = 'hardware_version'
|
||||
|
||||
# Air Humidifier CA
|
||||
ATTR_SPEED = 'speed'
|
||||
ATTR_DEPTH = 'depth'
|
||||
ATTR_DRY = 'dry'
|
||||
|
||||
# Map attributes to properties of the state object
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
|
||||
ATTR_TEMPERATURE: 'temperature',
|
||||
ATTR_HUMIDITY: 'humidity',
|
||||
ATTR_AIR_QUALITY_INDEX: 'aqi',
|
||||
ATTR_MODE: 'mode',
|
||||
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
|
||||
ATTR_FILTER_LIFE: 'filter_life_remaining',
|
||||
ATTR_FAVORITE_LEVEL: 'favorite_level',
|
||||
ATTR_CHILD_LOCK: 'child_lock',
|
||||
ATTR_LED: 'led',
|
||||
ATTR_MOTOR_SPEED: 'motor_speed',
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
|
||||
ATTR_PURIFY_VOLUME: 'purify_volume',
|
||||
ATTR_LEARN_MODE: 'learn_mode',
|
||||
ATTR_SLEEP_TIME: 'sleep_time',
|
||||
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
|
||||
ATTR_EXTRA_FEATURES: 'extra_features',
|
||||
ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported',
|
||||
ATTR_AUTO_DETECT: 'auto_detect',
|
||||
ATTR_USE_TIME: 'use_time',
|
||||
ATTR_BUTTON_PRESSED: 'button_pressed',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER = {
|
||||
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
||||
ATTR_BUZZER: 'buzzer',
|
||||
ATTR_LED_BRIGHTNESS: 'led_brightness',
|
||||
ATTR_SLEEP_MODE: 'sleep_mode',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = {
|
||||
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
||||
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
|
||||
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
|
||||
ATTR_FILTER_TYPE: 'filter_type',
|
||||
ATTR_ILLUMINANCE: 'illuminance',
|
||||
ATTR_MOTOR2_SPEED: 'motor2_speed',
|
||||
ATTR_VOLUME: 'volume',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
|
||||
# Common set isn't used here. It's a very basic version of the device.
|
||||
ATTR_AIR_QUALITY_INDEX: 'aqi',
|
||||
ATTR_MODE: 'mode',
|
||||
ATTR_LED: 'led',
|
||||
ATTR_BUZZER: 'buzzer',
|
||||
ATTR_CHILD_LOCK: 'child_lock',
|
||||
ATTR_ILLUMINANCE: 'illuminance',
|
||||
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
|
||||
ATTR_FILTER_LIFE: 'filter_life_remaining',
|
||||
ATTR_MOTOR_SPEED: 'motor_speed',
|
||||
# perhaps supported but unconfirmed
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
|
||||
ATTR_VOLUME: 'volume',
|
||||
ATTR_MOTOR2_SPEED: 'motor2_speed',
|
||||
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
|
||||
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
|
||||
ATTR_FILTER_TYPE: 'filter_type',
|
||||
ATTR_PURIFY_VOLUME: 'purify_volume',
|
||||
ATTR_LEARN_MODE: 'learn_mode',
|
||||
ATTR_SLEEP_TIME: 'sleep_time',
|
||||
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
|
||||
ATTR_EXTRA_FEATURES: 'extra_features',
|
||||
ATTR_AUTO_DETECT: 'auto_detect',
|
||||
ATTR_USE_TIME: 'use_time',
|
||||
ATTR_BUTTON_PRESSED: 'button_pressed',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
|
||||
ATTR_TEMPERATURE: 'temperature',
|
||||
ATTR_HUMIDITY: 'humidity',
|
||||
ATTR_MODE: 'mode',
|
||||
ATTR_BUZZER: 'buzzer',
|
||||
ATTR_CHILD_LOCK: 'child_lock',
|
||||
ATTR_TRANS_LEVEL: 'trans_level',
|
||||
ATTR_TARGET_HUMIDITY: 'target_humidity',
|
||||
ATTR_LED_BRIGHTNESS: 'led_brightness',
|
||||
ATTR_BUTTON_PRESSED: 'button_pressed',
|
||||
ATTR_USE_TIME: 'use_time',
|
||||
ATTR_HARDWARE_VERSION: 'hardware_version',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = {
|
||||
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER,
|
||||
ATTR_SPEED: 'speed',
|
||||
ATTR_DEPTH: 'depth',
|
||||
ATTR_DRY: 'dry',
|
||||
}
|
||||
|
||||
OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
|
||||
OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
|
||||
OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
|
||||
'Medium', 'High', 'Strong']
|
||||
|
||||
SUCCESS = ['ok']
|
||||
|
||||
FEATURE_SET_BUZZER = 1
|
||||
FEATURE_SET_LED = 2
|
||||
FEATURE_SET_CHILD_LOCK = 4
|
||||
FEATURE_SET_LED_BRIGHTNESS = 8
|
||||
FEATURE_SET_FAVORITE_LEVEL = 16
|
||||
FEATURE_SET_AUTO_DETECT = 32
|
||||
FEATURE_SET_LEARN_MODE = 64
|
||||
FEATURE_SET_VOLUME = 128
|
||||
FEATURE_RESET_FILTER = 256
|
||||
FEATURE_SET_EXTRA_FEATURES = 512
|
||||
FEATURE_SET_TARGET_HUMIDITY = 1024
|
||||
FEATURE_SET_DRY = 2048
|
||||
|
||||
FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER |
|
||||
FEATURE_SET_CHILD_LOCK)
|
||||
|
||||
FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC |
|
||||
FEATURE_SET_LED |
|
||||
FEATURE_SET_LED_BRIGHTNESS |
|
||||
FEATURE_SET_FAVORITE_LEVEL |
|
||||
FEATURE_SET_LEARN_MODE |
|
||||
FEATURE_RESET_FILTER |
|
||||
FEATURE_SET_EXTRA_FEATURES)
|
||||
|
||||
FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK |
|
||||
FEATURE_SET_LED |
|
||||
FEATURE_SET_FAVORITE_LEVEL |
|
||||
FEATURE_SET_AUTO_DETECT |
|
||||
FEATURE_SET_VOLUME)
|
||||
|
||||
FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC |
|
||||
FEATURE_SET_LED)
|
||||
|
||||
FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC |
|
||||
FEATURE_SET_LED_BRIGHTNESS |
|
||||
FEATURE_SET_TARGET_HUMIDITY)
|
||||
|
||||
FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER |
|
||||
FEATURE_SET_DRY)
|
||||
|
||||
SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on'
|
||||
SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off'
|
||||
SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on'
|
||||
SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off'
|
||||
SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on'
|
||||
SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off'
|
||||
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
|
||||
SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness'
|
||||
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
|
||||
SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on'
|
||||
SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off'
|
||||
SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on'
|
||||
SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off'
|
||||
SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume'
|
||||
SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter'
|
||||
SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features'
|
||||
SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity'
|
||||
SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on'
|
||||
SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off'
|
||||
|
||||
AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
@@ -74,6 +267,21 @@ SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16))
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||
vol.Required(ATTR_VOLUME):
|
||||
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100))
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||
vol.Required(ATTR_FEATURES):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||
vol.Required(ATTR_HUMIDITY):
|
||||
vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80]))
|
||||
})
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'},
|
||||
SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'},
|
||||
@@ -81,59 +289,99 @@ SERVICE_TO_METHOD = {
|
||||
SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'},
|
||||
SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'},
|
||||
SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'},
|
||||
SERVICE_SET_FAVORITE_LEVEL: {
|
||||
'method': 'async_set_favorite_level',
|
||||
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
|
||||
SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'},
|
||||
SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'},
|
||||
SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'},
|
||||
SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'},
|
||||
SERVICE_RESET_FILTER: {'method': 'async_reset_filter'},
|
||||
SERVICE_SET_LED_BRIGHTNESS: {
|
||||
'method': 'async_set_led_brightness',
|
||||
'schema': SERVICE_SCHEMA_LED_BRIGHTNESS},
|
||||
SERVICE_SET_FAVORITE_LEVEL: {
|
||||
'method': 'async_set_favorite_level',
|
||||
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
|
||||
SERVICE_SET_VOLUME: {
|
||||
'method': 'async_set_volume',
|
||||
'schema': SERVICE_SCHEMA_VOLUME},
|
||||
SERVICE_SET_EXTRA_FEATURES: {
|
||||
'method': 'async_set_extra_features',
|
||||
'schema': SERVICE_SCHEMA_EXTRA_FEATURES},
|
||||
SERVICE_SET_TARGET_HUMIDITY: {
|
||||
'method': 'async_set_target_humidity',
|
||||
'schema': SERVICE_SCHEMA_TARGET_HUMIDITY},
|
||||
SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'},
|
||||
SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'},
|
||||
}
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the air purifier from config."""
|
||||
from miio import AirPurifier, DeviceException
|
||||
if PLATFORM not in hass.data:
|
||||
hass.data[PLATFORM] = {}
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the miio fan device from config."""
|
||||
from miio import Device, DeviceException
|
||||
if DATA_KEY not in hass.data:
|
||||
hass.data[DATA_KEY] = {}
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
token = config.get(CONF_TOKEN)
|
||||
model = config.get(CONF_MODEL)
|
||||
|
||||
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
|
||||
unique_id = None
|
||||
|
||||
try:
|
||||
if model is None:
|
||||
try:
|
||||
miio_device = Device(host, token)
|
||||
device_info = miio_device.info()
|
||||
model = device_info.model
|
||||
unique_id = "{}-{}".format(model, device_info.mac_address)
|
||||
_LOGGER.info("%s %s %s detected",
|
||||
model,
|
||||
device_info.firmware_version,
|
||||
device_info.hardware_version)
|
||||
except DeviceException:
|
||||
raise PlatformNotReady
|
||||
|
||||
if model.startswith('zhimi.airpurifier.'):
|
||||
from miio import AirPurifier
|
||||
air_purifier = AirPurifier(host, token)
|
||||
device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
|
||||
elif model.startswith('zhimi.humidifier.'):
|
||||
from miio import AirHumidifier
|
||||
air_humidifier = AirHumidifier(host, token)
|
||||
device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Unsupported device found! Please create an issue at '
|
||||
'https://github.com/syssi/xiaomi_airpurifier/issues '
|
||||
'and provide the following data: %s', model)
|
||||
return False
|
||||
|
||||
xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier)
|
||||
hass.data[PLATFORM][host] = xiaomi_air_purifier
|
||||
except DeviceException:
|
||||
raise PlatformNotReady
|
||||
hass.data[DATA_KEY][host] = device
|
||||
async_add_devices([device], update_before_add=True)
|
||||
|
||||
async_add_devices([xiaomi_air_purifier], update_before_add=True)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
async def async_service_handler(service):
|
||||
"""Map services to methods on XiaomiAirPurifier."""
|
||||
method = SERVICE_TO_METHOD.get(service.service)
|
||||
params = {key: value for key, value in service.data.items()
|
||||
if key != ATTR_ENTITY_ID}
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
if entity_ids:
|
||||
devices = [device for device in hass.data[PLATFORM].values() if
|
||||
devices = [device for device in hass.data[DATA_KEY].values() if
|
||||
device.entity_id in entity_ids]
|
||||
else:
|
||||
devices = hass.data[PLATFORM].values()
|
||||
devices = hass.data[DATA_KEY].values()
|
||||
|
||||
update_tasks = []
|
||||
for device in devices:
|
||||
yield from getattr(device, method['method'])(**params)
|
||||
if not hasattr(device, method['method']):
|
||||
continue
|
||||
await getattr(device, method['method'])(**params)
|
||||
update_tasks.append(device.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
for air_purifier_service in SERVICE_TO_METHOD:
|
||||
schema = SERVICE_TO_METHOD[air_purifier_service].get(
|
||||
@@ -142,31 +390,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
DOMAIN, air_purifier_service, async_service_handler, schema=schema)
|
||||
|
||||
|
||||
class XiaomiAirPurifier(FanEntity):
|
||||
"""Representation of a Xiaomi Air Purifier."""
|
||||
class XiaomiGenericDevice(FanEntity):
|
||||
"""Representation of a generic Xiaomi device."""
|
||||
|
||||
def __init__(self, name, air_purifier):
|
||||
"""Initialize the air purifier."""
|
||||
def __init__(self, name, device, model, unique_id):
|
||||
"""Initialize the generic Xiaomi device."""
|
||||
self._name = name
|
||||
self._device = device
|
||||
self._model = model
|
||||
self._unique_id = unique_id
|
||||
|
||||
self._air_purifier = air_purifier
|
||||
self._available = False
|
||||
self._state = None
|
||||
self._state_attrs = {
|
||||
ATTR_AIR_QUALITY_INDEX: None,
|
||||
ATTR_TEMPERATURE: None,
|
||||
ATTR_HUMIDITY: None,
|
||||
ATTR_MODE: None,
|
||||
ATTR_FILTER_HOURS_USED: None,
|
||||
ATTR_FILTER_LIFE: None,
|
||||
ATTR_FAVORITE_LEVEL: None,
|
||||
ATTR_BUZZER: None,
|
||||
ATTR_CHILD_LOCK: None,
|
||||
ATTR_LED: None,
|
||||
ATTR_LED_BRIGHTNESS: None,
|
||||
ATTR_MOTOR_SPEED: None,
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: None,
|
||||
ATTR_PURIFY_VOLUME: None,
|
||||
ATTR_MODEL: self._model,
|
||||
}
|
||||
self._device_features = FEATURE_FLAGS_GENERIC
|
||||
self._skip_update = False
|
||||
|
||||
@property
|
||||
@@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll the fan."""
|
||||
"""Poll the device."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
@@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity):
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true when state is known."""
|
||||
return self._state is not None
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity):
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if fan is on."""
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def _try_command(self, mask_error, func, *args, **kwargs):
|
||||
"""Call an air purifier command handling error messages."""
|
||||
@staticmethod
|
||||
def _extract_value_from_attribute(state, attribute):
|
||||
value = getattr(state, attribute)
|
||||
if isinstance(value, Enum):
|
||||
return value.value
|
||||
|
||||
return value
|
||||
|
||||
async def _try_command(self, mask_error, func, *args, **kwargs):
|
||||
"""Call a miio device command handling error messages."""
|
||||
from miio import DeviceException
|
||||
try:
|
||||
result = yield from self.hass.async_add_job(
|
||||
result = await self.hass.async_add_job(
|
||||
partial(func, *args, **kwargs))
|
||||
|
||||
_LOGGER.debug("Response received from air purifier: %s", result)
|
||||
_LOGGER.debug("Response received from miio device: %s", result)
|
||||
|
||||
return result == SUCCESS
|
||||
except DeviceException as exc:
|
||||
_LOGGER.error(mask_error, exc)
|
||||
self._available = False
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
|
||||
"""Turn the fan on."""
|
||||
async def async_turn_on(self, speed: str = None,
|
||||
**kwargs) -> None:
|
||||
"""Turn the device on."""
|
||||
if speed:
|
||||
# If operation mode was set the device must not be turned on.
|
||||
result = yield from self.async_set_speed(speed)
|
||||
result = await self.async_set_speed(speed)
|
||||
else:
|
||||
result = yield from self._try_command(
|
||||
"Turning the air purifier on failed.", self._air_purifier.on)
|
||||
result = await self._try_command(
|
||||
"Turning the miio device on failed.", self._device.on)
|
||||
|
||||
if result:
|
||||
self._state = True
|
||||
self._skip_update = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self: ToggleEntity, **kwargs) -> None:
|
||||
"""Turn the fan off."""
|
||||
result = yield from self._try_command(
|
||||
"Turning the air purifier off failed.", self._air_purifier.off)
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the device off."""
|
||||
result = await self._try_command(
|
||||
"Turning the miio device off failed.", self._device.off)
|
||||
|
||||
if result:
|
||||
self._state = False
|
||||
self._skip_update = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_set_buzzer_on(self):
|
||||
"""Turn the buzzer on."""
|
||||
if self._device_features & FEATURE_SET_BUZZER == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the buzzer of the miio device on failed.",
|
||||
self._device.set_buzzer, True)
|
||||
|
||||
async def async_set_buzzer_off(self):
|
||||
"""Turn the buzzer off."""
|
||||
if self._device_features & FEATURE_SET_BUZZER == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the buzzer of the miio device off failed.",
|
||||
self._device.set_buzzer, False)
|
||||
|
||||
async def async_set_child_lock_on(self):
|
||||
"""Turn the child lock on."""
|
||||
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the child lock of the miio device on failed.",
|
||||
self._device.set_child_lock, True)
|
||||
|
||||
async def async_set_child_lock_off(self):
|
||||
"""Turn the child lock off."""
|
||||
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the child lock of the miio device off failed.",
|
||||
self._device.set_child_lock, False)
|
||||
|
||||
|
||||
class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||
"""Representation of a Xiaomi Air Purifier."""
|
||||
|
||||
def __init__(self, name, device, model, unique_id):
|
||||
"""Initialize the plug switch."""
|
||||
super().__init__(name, device, model, unique_id)
|
||||
|
||||
if self._model == MODEL_AIRPURIFIER_PRO:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO
|
||||
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO
|
||||
elif self._model == MODEL_AIRPURIFIER_V3:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3
|
||||
self._speed_list = OPERATION_MODES_AIRPURIFIER_V3
|
||||
else:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER
|
||||
self._speed_list = OPERATION_MODES_AIRPURIFIER
|
||||
|
||||
self._state_attrs.update(
|
||||
{attribute: None for attribute in self._available_attributes})
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch state from the device."""
|
||||
from miio import DeviceException
|
||||
|
||||
@@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity):
|
||||
return
|
||||
|
||||
try:
|
||||
state = yield from self.hass.async_add_job(
|
||||
self._air_purifier.status)
|
||||
state = await self.hass.async_add_job(
|
||||
self._device.status)
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
|
||||
self._available = True
|
||||
self._state = state.is_on
|
||||
self._state_attrs = {
|
||||
ATTR_TEMPERATURE: state.temperature,
|
||||
ATTR_HUMIDITY: state.humidity,
|
||||
ATTR_AIR_QUALITY_INDEX: state.aqi,
|
||||
ATTR_MODE: state.mode.value,
|
||||
ATTR_FILTER_HOURS_USED: state.filter_hours_used,
|
||||
ATTR_FILTER_LIFE: state.filter_life_remaining,
|
||||
ATTR_FAVORITE_LEVEL: state.favorite_level,
|
||||
ATTR_BUZZER: state.buzzer,
|
||||
ATTR_CHILD_LOCK: state.child_lock,
|
||||
ATTR_LED: state.led,
|
||||
ATTR_MOTOR_SPEED: state.motor_speed,
|
||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi,
|
||||
ATTR_PURIFY_VOLUME: state.purify_volume,
|
||||
}
|
||||
|
||||
if state.led_brightness:
|
||||
self._state_attrs[
|
||||
ATTR_LED_BRIGHTNESS] = state.led_brightness.value
|
||||
self._state_attrs.update(
|
||||
{key: self._extract_value_from_attribute(state, value) for
|
||||
key, value in self._available_attributes.items()})
|
||||
|
||||
except DeviceException as ex:
|
||||
self._state = None
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
@property
|
||||
def speed_list(self: ToggleEntity) -> list:
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
from miio.airpurifier import OperationMode
|
||||
return [mode.name for mode in OperationMode]
|
||||
return self._speed_list
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
@@ -294,70 +588,227 @@ class XiaomiAirPurifier(FanEntity):
|
||||
|
||||
return None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
_LOGGER.debug("Setting the operation mode to: %s", speed)
|
||||
if self.supported_features & SUPPORT_SET_SPEED == 0:
|
||||
return
|
||||
|
||||
from miio.airpurifier import OperationMode
|
||||
|
||||
yield from self._try_command(
|
||||
"Setting operation mode of the air purifier failed.",
|
||||
self._air_purifier.set_mode, OperationMode[speed.title()])
|
||||
_LOGGER.debug("Setting the operation mode to: %s", speed)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_buzzer_on(self):
|
||||
"""Turn the buzzer on."""
|
||||
yield from self._try_command(
|
||||
"Turning the buzzer of the air purifier on failed.",
|
||||
self._air_purifier.set_buzzer, True)
|
||||
await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode, OperationMode[speed.title()])
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_buzzer_off(self):
|
||||
"""Turn the buzzer off."""
|
||||
yield from self._try_command(
|
||||
"Turning the buzzer of the air purifier off failed.",
|
||||
self._air_purifier.set_buzzer, False)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_led_on(self):
|
||||
async def async_set_led_on(self):
|
||||
"""Turn the led on."""
|
||||
yield from self._try_command(
|
||||
"Turning the led of the air purifier off failed.",
|
||||
self._air_purifier.set_led, True)
|
||||
if self._device_features & FEATURE_SET_LED == 0:
|
||||
return
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_led_off(self):
|
||||
await self._try_command(
|
||||
"Turning the led of the miio device off failed.",
|
||||
self._device.set_led, True)
|
||||
|
||||
async def async_set_led_off(self):
|
||||
"""Turn the led off."""
|
||||
yield from self._try_command(
|
||||
"Turning the led of the air purifier off failed.",
|
||||
self._air_purifier.set_led, False)
|
||||
if self._device_features & FEATURE_SET_LED == 0:
|
||||
return
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_child_lock_on(self):
|
||||
"""Turn the child lock on."""
|
||||
yield from self._try_command(
|
||||
"Turning the child lock of the air purifier on failed.",
|
||||
self._air_purifier.set_child_lock, True)
|
||||
await self._try_command(
|
||||
"Turning the led of the miio device off failed.",
|
||||
self._device.set_led, False)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_child_lock_off(self):
|
||||
"""Turn the child lock off."""
|
||||
yield from self._try_command(
|
||||
"Turning the child lock of the air purifier off failed.",
|
||||
self._air_purifier.set_child_lock, False)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_led_brightness(self, brightness: int = 2):
|
||||
async def async_set_led_brightness(self, brightness: int = 2):
|
||||
"""Set the led brightness."""
|
||||
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
|
||||
return
|
||||
|
||||
from miio.airpurifier import LedBrightness
|
||||
|
||||
yield from self._try_command(
|
||||
"Setting the led brightness of the air purifier failed.",
|
||||
self._air_purifier.set_led_brightness, LedBrightness(brightness))
|
||||
await self._try_command(
|
||||
"Setting the led brightness of the miio device failed.",
|
||||
self._device.set_led_brightness, LedBrightness(brightness))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_favorite_level(self, level: int = 1):
|
||||
async def async_set_favorite_level(self, level: int = 1):
|
||||
"""Set the favorite level."""
|
||||
yield from self._try_command(
|
||||
"Setting the favorite level of the air purifier failed.",
|
||||
self._air_purifier.set_favorite_level, level)
|
||||
if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Setting the favorite level of the miio device failed.",
|
||||
self._device.set_favorite_level, level)
|
||||
|
||||
async def async_set_auto_detect_on(self):
|
||||
"""Turn the auto detect on."""
|
||||
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the auto detect of the miio device on failed.",
|
||||
self._device.set_auto_detect, True)
|
||||
|
||||
async def async_set_auto_detect_off(self):
|
||||
"""Turn the auto detect off."""
|
||||
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the auto detect of the miio device off failed.",
|
||||
self._device.set_auto_detect, False)
|
||||
|
||||
async def async_set_learn_mode_on(self):
|
||||
"""Turn the learn mode on."""
|
||||
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the learn mode of the miio device on failed.",
|
||||
self._device.set_learn_mode, True)
|
||||
|
||||
async def async_set_learn_mode_off(self):
|
||||
"""Turn the learn mode off."""
|
||||
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the learn mode of the miio device off failed.",
|
||||
self._device.set_learn_mode, False)
|
||||
|
||||
async def async_set_volume(self, volume: int = 50):
|
||||
"""Set the sound volume."""
|
||||
if self._device_features & FEATURE_SET_VOLUME == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Setting the sound volume of the miio device failed.",
|
||||
self._device.set_volume, volume)
|
||||
|
||||
async def async_set_extra_features(self, features: int = 1):
|
||||
"""Set the extra features."""
|
||||
if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Setting the extra features of the miio device failed.",
|
||||
self._device.set_extra_features, features)
|
||||
|
||||
async def async_reset_filter(self):
|
||||
"""Reset the filter lifetime and usage."""
|
||||
if self._device_features & FEATURE_RESET_FILTER == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Resetting the filter lifetime of the miio device failed.",
|
||||
self._device.reset_filter)
|
||||
|
||||
|
||||
class XiaomiAirHumidifier(XiaomiGenericDevice):
|
||||
"""Representation of a Xiaomi Air Humidifier."""
|
||||
|
||||
def __init__(self, name, device, model, unique_id):
|
||||
"""Initialize the plug switch."""
|
||||
from miio.airpurifier import OperationMode
|
||||
|
||||
super().__init__(name, device, model, unique_id)
|
||||
|
||||
if self._model == MODEL_AIRHUMIDIFIER_CA:
|
||||
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA
|
||||
self._speed_list = [mode.name for mode in OperationMode]
|
||||
else:
|
||||
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
|
||||
self._speed_list = [mode.name for mode in OperationMode if
|
||||
mode.name != 'Auto']
|
||||
|
||||
self._state_attrs.update(
|
||||
{attribute: None for attribute in self._available_attributes})
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch state from the device."""
|
||||
from miio import DeviceException
|
||||
|
||||
# On state change the device doesn't provide the new state immediately.
|
||||
if self._skip_update:
|
||||
self._skip_update = False
|
||||
return
|
||||
|
||||
try:
|
||||
state = await self.hass.async_add_job(self._device.status)
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
|
||||
self._available = True
|
||||
self._state = state.is_on
|
||||
self._state_attrs.update(
|
||||
{key: self._extract_value_from_attribute(state, value) for
|
||||
key, value in self._available_attributes.items()})
|
||||
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return self._speed_list
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
"""Return the current speed."""
|
||||
if self._state:
|
||||
from miio.airhumidifier import OperationMode
|
||||
|
||||
return OperationMode(self._state_attrs[ATTR_MODE]).name
|
||||
|
||||
return None
|
||||
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
if self.supported_features & SUPPORT_SET_SPEED == 0:
|
||||
return
|
||||
|
||||
from miio.airhumidifier import OperationMode
|
||||
|
||||
_LOGGER.debug("Setting the operation mode to: %s", speed)
|
||||
|
||||
await self._try_command(
|
||||
"Setting operation mode of the miio device failed.",
|
||||
self._device.set_mode, OperationMode[speed.title()])
|
||||
|
||||
async def async_set_led_brightness(self, brightness: int = 2):
|
||||
"""Set the led brightness."""
|
||||
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
|
||||
return
|
||||
|
||||
from miio.airhumidifier import LedBrightness
|
||||
|
||||
await self._try_command(
|
||||
"Setting the led brightness of the miio device failed.",
|
||||
self._device.set_led_brightness, LedBrightness(brightness))
|
||||
|
||||
async def async_set_target_humidity(self, humidity: int = 40):
|
||||
"""Set the target humidity."""
|
||||
if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Setting the target humidity of the miio device failed.",
|
||||
self._device.set_target_humidity, humidity)
|
||||
|
||||
async def async_set_dry_on(self):
|
||||
"""Turn the dry mode on."""
|
||||
if self._device_features & FEATURE_SET_DRY == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the dry mode of the miio device off failed.",
|
||||
self._device.set_dry, True)
|
||||
|
||||
async def async_set_dry_off(self):
|
||||
"""Turn the dry mode off."""
|
||||
if self._device_features & FEATURE_SET_DRY == 0:
|
||||
return
|
||||
|
||||
await self._try_command(
|
||||
"Turning the dry mode of the miio device off failed.",
|
||||
self._device.set_dry, False)
|
||||
|
||||
114
homeassistant/components/fan/zha.py
Normal file
114
homeassistant/components/fan/zha.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Fans on Zigbee Home Automation networks.
|
||||
|
||||
For more details on this platform, please refer to the documentation
|
||||
at https://home-assistant.io/components/fan.zha/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from homeassistant.components import zha
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
DEPENDENCIES = ['zha']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Additional speeds in zigbee's ZCL
|
||||
# Spec is unclear as to what this value means. On King Of Fans HBUniversal
|
||||
# receiver, this means Very High.
|
||||
SPEED_ON = 'on'
|
||||
# The fan speed is self-regulated
|
||||
SPEED_AUTO = 'auto'
|
||||
# When the heated/cooled space is occupied, the fan is always on
|
||||
SPEED_SMART = 'smart'
|
||||
|
||||
SPEED_LIST = [
|
||||
SPEED_OFF,
|
||||
SPEED_LOW,
|
||||
SPEED_MEDIUM,
|
||||
SPEED_HIGH,
|
||||
SPEED_ON,
|
||||
SPEED_AUTO,
|
||||
SPEED_SMART
|
||||
]
|
||||
|
||||
VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)}
|
||||
SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Zigbee Home Automation fans."""
|
||||
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
async_add_devices([ZhaFan(**discovery_info)], update_before_add=True)
|
||||
|
||||
|
||||
class ZhaFan(zha.Entity, FanEntity):
|
||||
"""Representation of a ZHA fan."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_SET_SPEED
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return SPEED_LIST
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if entity is on."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return False
|
||||
return self._state != SPEED_OFF
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, speed: str = None, **kwargs) -> None:
|
||||
"""Turn the entity on."""
|
||||
if speed is None:
|
||||
speed = SPEED_MEDIUM
|
||||
|
||||
yield from self.async_set_speed(speed)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the entity off."""
|
||||
yield from self.async_set_speed(SPEED_OFF)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_speed(self: FanEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
yield from self._endpoint.fan.write_attributes({
|
||||
'fan_mode': SPEED_TO_VALUE[speed]})
|
||||
|
||||
self._state = speed
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode'])
|
||||
new_value = result.get('fan_mode', None)
|
||||
self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if entity has to be polled for state.
|
||||
|
||||
False if entity pushes its state to HA.
|
||||
"""
|
||||
return False
|
||||
110
homeassistant/components/folder_watcher.py
Normal file
110
homeassistant/components/folder_watcher.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Component for monitoring activity on a folder.
|
||||
|
||||
For more details about this platform, refer to the documentation at
|
||||
https://home-assistant.io/components/folder_watcher/
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['watchdog==0.8.3']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FOLDER = 'folder'
|
||||
CONF_PATTERNS = 'patterns'
|
||||
DEFAULT_PATTERN = '*'
|
||||
DOMAIN = "folder_watcher"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_FOLDER): cv.isdir,
|
||||
vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the folder watcher."""
|
||||
conf = config[DOMAIN]
|
||||
for watcher in conf:
|
||||
path = watcher[CONF_FOLDER]
|
||||
patterns = watcher[CONF_PATTERNS]
|
||||
if not hass.config.is_allowed_path(path):
|
||||
_LOGGER.error("folder %s is not valid or allowed", path)
|
||||
return False
|
||||
Watcher(path, patterns, hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_event_handler(patterns, hass):
|
||||
""""Return the Watchdog EventHandler object."""
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
|
||||
class EventHandler(PatternMatchingEventHandler):
|
||||
"""Class for handling Watcher events."""
|
||||
|
||||
def __init__(self, patterns, hass):
|
||||
"""Initialise the EventHandler."""
|
||||
super().__init__(patterns)
|
||||
self.hass = hass
|
||||
|
||||
def process(self, event):
|
||||
"""On Watcher event, fire HA event."""
|
||||
_LOGGER.debug("process(%s)", event)
|
||||
if not event.is_directory:
|
||||
folder, file_name = os.path.split(event.src_path)
|
||||
self.hass.bus.fire(
|
||||
DOMAIN, {
|
||||
"event_type": event.event_type,
|
||||
'path': event.src_path,
|
||||
'file': file_name,
|
||||
'folder': folder,
|
||||
})
|
||||
|
||||
def on_modified(self, event):
|
||||
"""File modified."""
|
||||
self.process(event)
|
||||
|
||||
def on_moved(self, event):
|
||||
"""File moved."""
|
||||
self.process(event)
|
||||
|
||||
def on_created(self, event):
|
||||
"""File created."""
|
||||
self.process(event)
|
||||
|
||||
def on_deleted(self, event):
|
||||
"""File deleted."""
|
||||
self.process(event)
|
||||
|
||||
return EventHandler(patterns, hass)
|
||||
|
||||
|
||||
class Watcher():
|
||||
"""Class for starting Watchdog."""
|
||||
|
||||
def __init__(self, path, patterns, hass):
|
||||
"""Initialise the watchdog observer."""
|
||||
from watchdog.observers import Observer
|
||||
self._observer = Observer()
|
||||
self._observer.schedule(
|
||||
create_event_handler(patterns, hass),
|
||||
path,
|
||||
recursive=True)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
|
||||
|
||||
def startup(self, event):
|
||||
"""Start the watcher."""
|
||||
self._observer.start()
|
||||
|
||||
def shutdown(self, event):
|
||||
"""Shutdown the watcher."""
|
||||
self._observer.stop()
|
||||
self._observer.join()
|
||||
103
homeassistant/components/freedns.py
Normal file
103
homeassistant/components/freedns.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/freedns/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'freedns'
|
||||
|
||||
DEFAULT_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
TIMEOUT = 10
|
||||
UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php'
|
||||
|
||||
CONF_UPDATE_INTERVAL = 'update_interval'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Exclusive(CONF_URL, DOMAIN): cv.string,
|
||||
vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string,
|
||||
vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Initialize the FreeDNS component."""
|
||||
url = config[DOMAIN].get(CONF_URL)
|
||||
auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
|
||||
update_interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL)
|
||||
|
||||
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
|
||||
result = yield from _update_freedns(
|
||||
hass, session, url, auth_token)
|
||||
|
||||
if result is False:
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def update_domain_callback(now):
|
||||
"""Update the FreeDNS entry."""
|
||||
yield from _update_freedns(hass, session, url, auth_token)
|
||||
|
||||
hass.helpers.event.async_track_time_interval(
|
||||
update_domain_callback, update_interval)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _update_freedns(hass, session, url, auth_token):
|
||||
"""Update FreeDNS."""
|
||||
params = None
|
||||
|
||||
if url is None:
|
||||
url = UPDATE_URL
|
||||
|
||||
if auth_token is not None:
|
||||
params = {}
|
||||
params[auth_token] = ""
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
resp = yield from session.get(url, params=params)
|
||||
body = yield from resp.text()
|
||||
|
||||
if "has not changed" in body:
|
||||
# IP has not changed.
|
||||
_LOGGER.debug("FreeDNS update skipped: IP has not changed")
|
||||
return True
|
||||
|
||||
if "ERROR" not in body:
|
||||
_LOGGER.debug("Updating FreeDNS was successful: %s", body)
|
||||
return True
|
||||
|
||||
if "Invalid update URL" in body:
|
||||
_LOGGER.error("FreeDNS update token is invalid")
|
||||
else:
|
||||
_LOGGER.warning("Updating FreeDNS failed: %s", body)
|
||||
|
||||
except aiohttp.ClientError:
|
||||
_LOGGER.warning("Can't connect to FreeDNS API")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning("Timeout from FreeDNS API at %s", url)
|
||||
|
||||
return False
|
||||
@@ -24,7 +24,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180310.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180404.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
|
||||
@@ -17,7 +17,16 @@ from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES)
|
||||
from homeassistant.components import (
|
||||
switch, light, cover, media_player, group, fan, scene, script, climate,
|
||||
climate,
|
||||
cover,
|
||||
fan,
|
||||
group,
|
||||
input_boolean,
|
||||
light,
|
||||
media_player,
|
||||
scene,
|
||||
script,
|
||||
switch,
|
||||
)
|
||||
|
||||
from . import trait
|
||||
@@ -33,15 +42,16 @@ HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN_TO_GOOGLE_TYPES = {
|
||||
climate.DOMAIN: TYPE_THERMOSTAT,
|
||||
cover.DOMAIN: TYPE_SWITCH,
|
||||
fan.DOMAIN: TYPE_SWITCH,
|
||||
group.DOMAIN: TYPE_SWITCH,
|
||||
input_boolean.DOMAIN: TYPE_SWITCH,
|
||||
light.DOMAIN: TYPE_LIGHT,
|
||||
media_player.DOMAIN: TYPE_SWITCH,
|
||||
scene.DOMAIN: TYPE_SCENE,
|
||||
script.DOMAIN: TYPE_SCENE,
|
||||
switch.DOMAIN: TYPE_SWITCH,
|
||||
fan.DOMAIN: TYPE_SWITCH,
|
||||
light.DOMAIN: TYPE_LIGHT,
|
||||
cover.DOMAIN: TYPE_SWITCH,
|
||||
media_player.DOMAIN: TYPE_SWITCH,
|
||||
climate.DOMAIN: TYPE_THERMOSTAT,
|
||||
}
|
||||
|
||||
|
||||
@@ -84,9 +94,16 @@ class _GoogleEntity:
|
||||
|
||||
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
|
||||
"""
|
||||
traits = self.traits()
|
||||
state = self.state
|
||||
|
||||
# When a state is unavailable, the attributes that describe
|
||||
# capabilities will be stripped. For example, a light entity will miss
|
||||
# the min/max mireds. Therefore they will be excluded from a sync.
|
||||
if state.state == STATE_UNAVAILABLE:
|
||||
return None
|
||||
|
||||
traits = self.traits()
|
||||
|
||||
# Found no supported traits for this entity
|
||||
if not traits:
|
||||
return None
|
||||
|
||||
@@ -243,7 +243,7 @@ class ColorSpectrumTrait(_Trait):
|
||||
if domain != light.DOMAIN:
|
||||
return False
|
||||
|
||||
return features & (light.SUPPORT_RGB_COLOR | light.SUPPORT_XY_COLOR)
|
||||
return features & light.SUPPORT_COLOR
|
||||
|
||||
def sync_attributes(self):
|
||||
"""Return color spectrum attributes for a sync request."""
|
||||
@@ -254,13 +254,11 @@ class ColorSpectrumTrait(_Trait):
|
||||
"""Return color spectrum query attributes."""
|
||||
response = {}
|
||||
|
||||
# No need to handle XY color because light component will always
|
||||
# convert XY to RGB if possible (which is when brightness is available)
|
||||
color_rgb = self.state.attributes.get(light.ATTR_RGB_COLOR)
|
||||
if color_rgb is not None:
|
||||
color_hs = self.state.attributes.get(light.ATTR_HS_COLOR)
|
||||
if color_hs is not None:
|
||||
response['color'] = {
|
||||
'spectrumRGB': int(color_util.color_rgb_to_hex(
|
||||
color_rgb[0], color_rgb[1], color_rgb[2]), 16),
|
||||
*color_util.color_hs_to_RGB(*color_hs)), 16),
|
||||
}
|
||||
|
||||
return response
|
||||
@@ -274,11 +272,12 @@ class ColorSpectrumTrait(_Trait):
|
||||
"""Execute a color spectrum command."""
|
||||
# Convert integer to hex format and left pad with 0's till length 6
|
||||
hex_value = "{0:06x}".format(params['color']['spectrumRGB'])
|
||||
color = color_util.rgb_hex_to_rgb_list(hex_value)
|
||||
color = color_util.color_RGB_to_hs(
|
||||
*color_util.rgb_hex_to_rgb_list(hex_value))
|
||||
|
||||
await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: self.state.entity_id,
|
||||
light.ATTR_RGB_COLOR: color
|
||||
light.ATTR_HS_COLOR: color
|
||||
}, blocking=True)
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
DOMAIN = 'group'
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ def async_setup(hass, config):
|
||||
|
||||
if 'frontend' in hass.config.components:
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
'hassio', 'Hass.io', 'mdi:access-point-network')
|
||||
'hassio', 'Hass.io', 'mdi:home-assistant')
|
||||
|
||||
if 'http' in config:
|
||||
yield from hassio.update_hass_api(config['http'])
|
||||
|
||||
@@ -239,15 +239,16 @@ def get_state(hass, utc_point_in_time, entity_id, run=None):
|
||||
def async_setup(hass, config):
|
||||
"""Set up the history hooks."""
|
||||
filters = Filters()
|
||||
exclude = config[DOMAIN].get(CONF_EXCLUDE)
|
||||
conf = config.get(DOMAIN, {})
|
||||
exclude = conf.get(CONF_EXCLUDE)
|
||||
if exclude:
|
||||
filters.excluded_entities = exclude.get(CONF_ENTITIES, [])
|
||||
filters.excluded_domains = exclude.get(CONF_DOMAINS, [])
|
||||
include = config[DOMAIN].get(CONF_INCLUDE)
|
||||
include = conf.get(CONF_INCLUDE)
|
||||
if include:
|
||||
filters.included_entities = include.get(CONF_ENTITIES, [])
|
||||
filters.included_domains = include.get(CONF_DOMAINS, [])
|
||||
use_include_order = config[DOMAIN].get(CONF_ORDER)
|
||||
use_include_order = conf.get(CONF_ORDER)
|
||||
|
||||
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
@@ -308,7 +309,7 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
result = yield from hass.async_add_job(
|
||||
get_significant_states, hass, start_time, end_time,
|
||||
entity_ids, self.filters, include_start_time_state)
|
||||
result = result.values()
|
||||
result = list(result.values())
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
elapsed = time.perf_counter() - timer_start
|
||||
_LOGGER.debug(
|
||||
@@ -318,7 +319,6 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
# by any entities explicitly included in the configuration.
|
||||
|
||||
if self.use_include_order:
|
||||
result = list(result)
|
||||
sorted_result = []
|
||||
for order_entity in self.filters.included_entities:
|
||||
for state_list in result:
|
||||
|
||||
@@ -3,154 +3,202 @@
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/homekit/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from zlib import adler32
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.climate import (
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
from homeassistant.components.cover import SUPPORT_SET_POSITION
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
||||
from homeassistant.util import get_local_ip
|
||||
from homeassistant.util.decorator import Registry
|
||||
from .const import (
|
||||
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
|
||||
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START)
|
||||
from .util import (
|
||||
validate_entity_config, show_setup_message)
|
||||
|
||||
TYPES = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$"
|
||||
|
||||
DOMAIN = 'homekit'
|
||||
REQUIREMENTS = ['HAP-python==1.1.7']
|
||||
|
||||
BRIDGE_NAME = 'Home Assistant'
|
||||
CONF_PIN_CODE = 'pincode'
|
||||
|
||||
HOMEKIT_FILE = '.homekit.state'
|
||||
|
||||
|
||||
def valid_pin(value):
|
||||
"""Validate pin code value."""
|
||||
match = re.match(_RE_VALID_PINCODE, str(value).strip())
|
||||
if not match:
|
||||
raise vol.Invalid("Pin must be in the format: '123-45-678'")
|
||||
return match.group(0)
|
||||
REQUIREMENTS = ['HAP-python==1.1.8']
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All({
|
||||
vol.Optional(CONF_PORT, default=51826): vol.Coerce(int),
|
||||
vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean,
|
||||
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Setup the HomeKit component."""
|
||||
_LOGGER.debug("Begin setup HomeKit")
|
||||
_LOGGER.debug('Begin setup HomeKit')
|
||||
|
||||
conf = config[DOMAIN]
|
||||
port = conf.get(CONF_PORT)
|
||||
pin = str.encode(conf.get(CONF_PIN_CODE))
|
||||
port = conf[CONF_PORT]
|
||||
auto_start = conf[CONF_AUTO_START]
|
||||
entity_filter = conf[CONF_FILTER]
|
||||
entity_config = conf[CONF_ENTITY_CONFIG]
|
||||
|
||||
homekit = HomeKit(hass, port)
|
||||
homekit.setup_bridge(pin)
|
||||
homekit = HomeKit(hass, port, entity_filter, entity_config)
|
||||
homekit.setup()
|
||||
|
||||
if auto_start:
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start)
|
||||
return True
|
||||
|
||||
def handle_homekit_service_start(service):
|
||||
"""Handle start HomeKit service call."""
|
||||
if homekit.started:
|
||||
_LOGGER.warning('HomeKit is already running')
|
||||
return
|
||||
homekit.start()
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START,
|
||||
handle_homekit_service_start)
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, homekit.start_driver)
|
||||
return True
|
||||
|
||||
|
||||
def import_types():
|
||||
"""Import all types from files in the HomeKit directory."""
|
||||
_LOGGER.debug("Import type files.")
|
||||
# pylint: disable=unused-variable
|
||||
from . import ( # noqa F401
|
||||
covers, security_systems, sensors, switches, thermostats)
|
||||
|
||||
|
||||
def get_accessory(hass, state):
|
||||
def get_accessory(hass, state, aid, config):
|
||||
"""Take state and return an accessory object if supported."""
|
||||
if not aid:
|
||||
_LOGGER.warning('The entitiy "%s" is not supported, since it '
|
||||
'generates an invalid aid, please change it.',
|
||||
state.entity_id)
|
||||
return None
|
||||
|
||||
if state.domain == 'sensor':
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"",
|
||||
_LOGGER.debug('Add "%s" as "%s"',
|
||||
state.entity_id, 'TemperatureSensor')
|
||||
return TYPES['TemperatureSensor'](hass, state.entity_id,
|
||||
state.name)
|
||||
state.name, aid=aid)
|
||||
elif unit == '%':
|
||||
_LOGGER.debug('Add "%s" as %s"',
|
||||
state.entity_id, 'HumiditySensor')
|
||||
return TYPES['HumiditySensor'](hass, state.entity_id, state.name,
|
||||
aid=aid)
|
||||
|
||||
elif state.domain == 'cover':
|
||||
# Only add covers that support set_cover_position
|
||||
if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4:
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"",
|
||||
state.entity_id, 'Window')
|
||||
return TYPES['Window'](hass, state.entity_id, state.name)
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if features & SUPPORT_SET_POSITION:
|
||||
_LOGGER.debug('Add "%s" as "%s"',
|
||||
state.entity_id, 'WindowCovering')
|
||||
return TYPES['WindowCovering'](hass, state.entity_id, state.name,
|
||||
aid=aid)
|
||||
|
||||
elif state.domain == 'alarm_control_panel':
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id,
|
||||
'SecuritySystem')
|
||||
return TYPES['SecuritySystem'](hass, state.entity_id, state.name)
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
|
||||
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
|
||||
alarm_code=config.get(ATTR_CODE),
|
||||
aid=aid)
|
||||
|
||||
elif state.domain == 'climate':
|
||||
support_auto = False
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH
|
||||
# Check if climate device supports auto mode
|
||||
if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \
|
||||
and (features & SUPPORT_TARGET_TEMPERATURE_LOW):
|
||||
support_auto = True
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat')
|
||||
support_auto = bool(features & support_temp_range)
|
||||
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
|
||||
return TYPES['Thermostat'](hass, state.entity_id,
|
||||
state.name, support_auto)
|
||||
state.name, support_auto, aid=aid)
|
||||
|
||||
elif state.domain == 'light':
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light')
|
||||
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
|
||||
|
||||
elif state.domain == 'switch' or state.domain == 'remote' \
|
||||
or state.domain == 'input_boolean':
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch')
|
||||
return TYPES['Switch'](hass, state.entity_id, state.name)
|
||||
or state.domain == 'input_boolean' or state.domain == 'script':
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch')
|
||||
return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def generate_aid(entity_id):
|
||||
"""Generate accessory aid with zlib adler32."""
|
||||
aid = adler32(entity_id.encode('utf-8'))
|
||||
if aid == 0 or aid == 1:
|
||||
return None
|
||||
return aid
|
||||
|
||||
|
||||
class HomeKit():
|
||||
"""Class to handle all actions between HomeKit and Home Assistant."""
|
||||
|
||||
def __init__(self, hass, port):
|
||||
def __init__(self, hass, port, entity_filter, entity_config):
|
||||
"""Initialize a HomeKit object."""
|
||||
self._hass = hass
|
||||
self._port = port
|
||||
self._filter = entity_filter
|
||||
self._config = entity_config
|
||||
self.started = False
|
||||
|
||||
self.bridge = None
|
||||
self.driver = None
|
||||
|
||||
def setup_bridge(self, pin):
|
||||
"""Setup the bridge component to track all accessories."""
|
||||
from .accessories import HomeBridge
|
||||
self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin)
|
||||
def setup(self):
|
||||
"""Setup bridge and accessory driver."""
|
||||
from .accessories import HomeBridge, HomeDriver
|
||||
|
||||
def start_driver(self, event):
|
||||
"""Start the accessory driver."""
|
||||
from pyhap.accessory_driver import AccessoryDriver
|
||||
self._hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self.stop_driver)
|
||||
self._hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
|
||||
import_types()
|
||||
_LOGGER.debug("Start adding accessories.")
|
||||
for state in self._hass.states.all():
|
||||
acc = get_accessory(self._hass, state)
|
||||
if acc is not None:
|
||||
self.bridge.add_accessory(acc)
|
||||
|
||||
ip_address = get_local_ip()
|
||||
path = self._hass.config.path(HOMEKIT_FILE)
|
||||
self.driver = AccessoryDriver(self.bridge, self._port,
|
||||
ip_address, path)
|
||||
_LOGGER.debug("Driver started")
|
||||
self.bridge = HomeBridge(self._hass)
|
||||
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
|
||||
|
||||
def add_bridge_accessory(self, state):
|
||||
"""Try adding accessory to bridge if configured beforehand."""
|
||||
if not state or not self._filter(state.entity_id):
|
||||
return
|
||||
aid = generate_aid(state.entity_id)
|
||||
conf = self._config.pop(state.entity_id, {})
|
||||
acc = get_accessory(self._hass, state, aid, conf)
|
||||
if acc is not None:
|
||||
self.bridge.add_accessory(acc)
|
||||
|
||||
def start(self, *args):
|
||||
"""Start the accessory driver."""
|
||||
if self.started:
|
||||
return
|
||||
self.started = True
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
from . import ( # noqa F401
|
||||
type_covers, type_lights, type_security_systems, type_sensors,
|
||||
type_switches, type_thermostats)
|
||||
|
||||
for state in self._hass.states.all():
|
||||
self.add_bridge_accessory(state)
|
||||
self.bridge.set_broker(self.driver)
|
||||
|
||||
if not self.bridge.paired:
|
||||
show_setup_message(self.bridge, self._hass)
|
||||
|
||||
_LOGGER.debug('Driver start')
|
||||
self.driver.start()
|
||||
|
||||
def stop_driver(self, event):
|
||||
def stop(self, *args):
|
||||
"""Stop the accessory driver."""
|
||||
_LOGGER.debug("Driver stop")
|
||||
if self.driver is not None:
|
||||
if not self.started:
|
||||
return
|
||||
|
||||
_LOGGER.debug('Driver stop')
|
||||
if self.driver and self.driver.run_sentinel:
|
||||
self.driver.stop()
|
||||
|
||||
@@ -2,15 +2,33 @@
|
||||
import logging
|
||||
|
||||
from pyhap.accessory import Accessory, Bridge, Category
|
||||
from pyhap.accessory_driver import AccessoryDriver
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from .const import (
|
||||
SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER,
|
||||
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER)
|
||||
|
||||
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
|
||||
MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
|
||||
CHAR_NAME, CHAR_SERIAL_NUMBER)
|
||||
from .util import (
|
||||
show_setup_message, dismiss_setup_message)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def add_preload_service(acc, service, chars=None):
|
||||
"""Define and return a service to be available for the accessory."""
|
||||
from pyhap.loader import get_serv_loader, get_char_loader
|
||||
service = get_serv_loader().get(service)
|
||||
if chars:
|
||||
chars = chars if isinstance(chars, list) else [chars]
|
||||
for char_name in chars:
|
||||
char = get_char_loader().get(char_name)
|
||||
service.add_characteristic(char)
|
||||
acc.add_service(service)
|
||||
return service
|
||||
|
||||
|
||||
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
|
||||
serial_number='0000'):
|
||||
"""Set the default accessory information."""
|
||||
@@ -21,50 +39,60 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
|
||||
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
|
||||
|
||||
|
||||
def add_preload_service(acc, service, chars=None, opt_chars=None):
|
||||
"""Define and return a service to be available for the accessory."""
|
||||
from pyhap.loader import get_serv_loader, get_char_loader
|
||||
service = get_serv_loader().get(service)
|
||||
if chars:
|
||||
chars = chars if isinstance(chars, list) else [chars]
|
||||
for char_name in chars:
|
||||
char = get_char_loader().get(char_name)
|
||||
service.add_characteristic(char)
|
||||
if opt_chars:
|
||||
opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars]
|
||||
for opt_char_name in opt_chars:
|
||||
opt_char = get_char_loader().get(opt_char_name)
|
||||
service.add_opt_characteristic(opt_char)
|
||||
acc.add_service(service)
|
||||
return service
|
||||
|
||||
|
||||
def override_properties(char, new_properties):
|
||||
"""Override characteristic property values."""
|
||||
char.properties.update(new_properties)
|
||||
|
||||
|
||||
class HomeAccessory(Accessory):
|
||||
"""Class to extend the Accessory class."""
|
||||
"""Adapter class for Accessory."""
|
||||
|
||||
def __init__(self, display_name, model, category='OTHER', **kwargs):
|
||||
# pylint: disable=no-member
|
||||
|
||||
def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
|
||||
category='OTHER', **kwargs):
|
||||
"""Initialize a Accessory object."""
|
||||
super().__init__(display_name, **kwargs)
|
||||
set_accessory_info(self, display_name, model)
|
||||
super().__init__(name, **kwargs)
|
||||
set_accessory_info(self, name, model)
|
||||
self.category = getattr(Category, category, Category.OTHER)
|
||||
|
||||
def _set_services(self):
|
||||
add_preload_service(self, SERV_ACCESSORY_INFO)
|
||||
|
||||
def run(self):
|
||||
"""Method called by accessory after driver is started."""
|
||||
state = self.hass.states.get(self.entity_id)
|
||||
self.update_state(new_state=state)
|
||||
async_track_state_change(
|
||||
self.hass, self.entity_id, self.update_state)
|
||||
|
||||
|
||||
class HomeBridge(Bridge):
|
||||
"""Class to extend the Bridge class."""
|
||||
"""Adapter class for Bridge."""
|
||||
|
||||
def __init__(self, display_name, model, pincode, **kwargs):
|
||||
def __init__(self, hass, name=BRIDGE_NAME,
|
||||
model=BRIDGE_MODEL, **kwargs):
|
||||
"""Initialize a Bridge object."""
|
||||
super().__init__(display_name, pincode=pincode, **kwargs)
|
||||
set_accessory_info(self, display_name, model)
|
||||
super().__init__(name, **kwargs)
|
||||
set_accessory_info(self, name, model)
|
||||
self.hass = hass
|
||||
|
||||
def _set_services(self):
|
||||
add_preload_service(self, SERV_ACCESSORY_INFO)
|
||||
add_preload_service(self, SERV_BRIDGING_STATE)
|
||||
|
||||
def setup_message(self):
|
||||
"""Prevent print of pyhap setup message to terminal."""
|
||||
pass
|
||||
|
||||
def add_paired_client(self, client_uuid, client_public):
|
||||
"""Override super function to dismiss setup message if paired."""
|
||||
super().add_paired_client(client_uuid, client_public)
|
||||
dismiss_setup_message(self.hass)
|
||||
|
||||
def remove_paired_client(self, client_uuid):
|
||||
"""Override super function to show setup message if unpaired."""
|
||||
super().remove_paired_client(client_uuid)
|
||||
show_setup_message(self, self.hass)
|
||||
|
||||
|
||||
class HomeDriver(AccessoryDriver):
|
||||
"""Adapter class for AccessoryDriver."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize a AccessoryDriver object."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -1,31 +1,67 @@
|
||||
"""Constants used be the HomeKit component."""
|
||||
# #### MISC ####
|
||||
DOMAIN = 'homekit'
|
||||
HOMEKIT_FILE = '.homekit.state'
|
||||
HOMEKIT_NOTIFY_ID = 4663548
|
||||
|
||||
# #### CONFIG ####
|
||||
CONF_AUTO_START = 'auto_start'
|
||||
CONF_ENTITY_CONFIG = 'entity_config'
|
||||
CONF_FILTER = 'filter'
|
||||
|
||||
# #### CONFIG DEFAULTS ####
|
||||
DEFAULT_AUTO_START = True
|
||||
DEFAULT_PORT = 51827
|
||||
|
||||
# #### HOMEKIT COMPONENT SERVICES ####
|
||||
SERVICE_HOMEKIT_START = 'start'
|
||||
|
||||
# #### STRING CONSTANTS ####
|
||||
ACCESSORY_MODEL = 'homekit.accessory'
|
||||
ACCESSORY_NAME = 'Home Accessory'
|
||||
BRIDGE_MODEL = 'homekit.bridge'
|
||||
BRIDGE_NAME = 'Home Assistant'
|
||||
MANUFACTURER = 'HomeAssistant'
|
||||
|
||||
# Services
|
||||
# #### Categories ####
|
||||
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
|
||||
CATEGORY_LIGHT = 'LIGHTBULB'
|
||||
CATEGORY_SENSOR = 'SENSOR'
|
||||
CATEGORY_SWITCH = 'SWITCH'
|
||||
CATEGORY_THERMOSTAT = 'THERMOSTAT'
|
||||
CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
|
||||
|
||||
|
||||
# #### Services ####
|
||||
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
||||
SERV_BRIDGING_STATE = 'BridgingState'
|
||||
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
|
||||
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
|
||||
# StatusLowBattery, Name
|
||||
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
|
||||
SERV_SECURITY_SYSTEM = 'SecuritySystem'
|
||||
SERV_SWITCH = 'Switch'
|
||||
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
||||
SERV_THERMOSTAT = 'Thermostat'
|
||||
SERV_WINDOW_COVERING = 'WindowCovering'
|
||||
|
||||
# Characteristics
|
||||
CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
|
||||
CHAR_CATEGORY = 'Category'
|
||||
|
||||
# #### Characteristics ####
|
||||
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
|
||||
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
||||
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition'
|
||||
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
|
||||
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
||||
CHAR_LINK_QUALITY = 'LinkQuality'
|
||||
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
|
||||
CHAR_MANUFACTURER = 'Manufacturer'
|
||||
CHAR_MODEL = 'Model'
|
||||
CHAR_NAME = 'Name'
|
||||
CHAR_ON = 'On'
|
||||
CHAR_ON = 'On' # boolean
|
||||
CHAR_POSITION_STATE = 'PositionState'
|
||||
CHAR_REACHABLE = 'Reachable'
|
||||
CHAR_SATURATION = 'Saturation' # percent
|
||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
|
||||
CHAR_TARGET_POSITION = 'TargetPosition'
|
||||
@@ -33,5 +69,5 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
|
||||
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
|
||||
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
|
||||
|
||||
# Properties
|
||||
# #### Properties ####
|
||||
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Class to hold all cover accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import ATTR_CURRENT_POSITION
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import (
|
||||
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
|
||||
CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('Window')
|
||||
class Window(HomeAccessory):
|
||||
"""Generate a Window accessory for a cover entity.
|
||||
|
||||
The cover entity must support: set_cover_position.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name):
|
||||
"""Initialize a Window accessory object."""
|
||||
super().__init__(display_name, entity_id, 'WINDOW')
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
|
||||
self.current_position = None
|
||||
self.homekit_target = None
|
||||
|
||||
self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
|
||||
self.char_current_position = self.serv_cover. \
|
||||
get_characteristic(CHAR_CURRENT_POSITION)
|
||||
self.char_target_position = self.serv_cover. \
|
||||
get_characteristic(CHAR_TARGET_POSITION)
|
||||
self.char_position_state = self.serv_cover. \
|
||||
get_characteristic(CHAR_POSITION_STATE)
|
||||
self.char_current_position.value = 0
|
||||
self.char_target_position.value = 0
|
||||
self.char_position_state.value = 0
|
||||
|
||||
self.char_target_position.setter_callback = self.move_cover
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_cover_position(new_state=state)
|
||||
|
||||
async_track_state_change(
|
||||
self._hass, self._entity_id, self.update_cover_position)
|
||||
|
||||
def move_cover(self, value):
|
||||
"""Move cover to value if call came from HomeKit."""
|
||||
if value != self.current_position:
|
||||
_LOGGER.debug("%s: Set position to %d", self._entity_id, value)
|
||||
self.homekit_target = value
|
||||
if value > self.current_position:
|
||||
self.char_position_state.set_value(1)
|
||||
elif value < self.current_position:
|
||||
self.char_position_state.set_value(0)
|
||||
self._hass.services.call(
|
||||
'cover', 'set_cover_position',
|
||||
{'entity_id': self._entity_id, 'position': value})
|
||||
|
||||
def update_cover_position(self, entity_id=None, old_state=None,
|
||||
new_state=None):
|
||||
"""Update cover position after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
current_position = new_state.attributes[ATTR_CURRENT_POSITION]
|
||||
if current_position is None:
|
||||
return
|
||||
self.current_position = int(current_position)
|
||||
self.char_current_position.set_value(self.current_position)
|
||||
|
||||
if self.homekit_target is None or \
|
||||
abs(self.current_position - self.homekit_target) < 6:
|
||||
self.char_target_position.set_value(self.current_position)
|
||||
self.char_position_state.set_value(2)
|
||||
self.homekit_target = None
|
||||
@@ -1,72 +0,0 @@
|
||||
"""Class to hold all sensor accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import (
|
||||
HomeAccessory, add_preload_service, override_properties)
|
||||
from .const import (
|
||||
SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def calc_temperature(state, unit=TEMP_CELSIUS):
|
||||
"""Calculate temperature from state and unit.
|
||||
|
||||
Always return temperature as Celsius value.
|
||||
Conversion is handled on the device.
|
||||
"""
|
||||
try:
|
||||
value = float(state)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
return round((value - 32) / 1.8, 2) if unit == TEMP_FAHRENHEIT else value
|
||||
|
||||
|
||||
@TYPES.register('TemperatureSensor')
|
||||
class TemperatureSensor(HomeAccessory):
|
||||
"""Generate a TemperatureSensor accessory for a temperature sensor.
|
||||
|
||||
Sensor entity must return temperature in °C, °F.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name):
|
||||
"""Initialize a TemperatureSensor accessory object."""
|
||||
super().__init__(display_name, entity_id, 'SENSOR')
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
|
||||
self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
|
||||
self.char_temp = self.serv_temp. \
|
||||
get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
||||
override_properties(self.char_temp, PROP_CELSIUS)
|
||||
self.char_temp.value = 0
|
||||
self.unit = None
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_temperature(new_state=state)
|
||||
|
||||
async_track_state_change(
|
||||
self._hass, self._entity_id, self.update_temperature)
|
||||
|
||||
def update_temperature(self, entity_id=None, old_state=None,
|
||||
new_state=None):
|
||||
"""Update temperature after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
|
||||
temperature = calc_temperature(new_state.state, unit)
|
||||
if temperature is not None:
|
||||
self.char_temp.set_value(temperature)
|
||||
_LOGGER.debug("%s: Current temperature set to %d°C",
|
||||
self._entity_id, temperature)
|
||||
4
homeassistant/components/homekit/services.yaml
Normal file
4
homeassistant/components/homekit/services.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
# Describes the format for available HomeKit services
|
||||
|
||||
start:
|
||||
description: Starts the HomeKit component driver.
|
||||
@@ -1,62 +0,0 @@
|
||||
"""Class to hold all switch accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import SERV_SWITCH, CHAR_ON
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('Switch')
|
||||
class Switch(HomeAccessory):
|
||||
"""Generate a Switch accessory."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name):
|
||||
"""Initialize a Switch accessory object to represent a remote."""
|
||||
super().__init__(display_name, entity_id, 'SWITCH')
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self._domain = split_entity_id(entity_id)[0]
|
||||
|
||||
self.flag_target_state = False
|
||||
|
||||
self.service_switch = add_preload_service(self, SERV_SWITCH)
|
||||
self.char_on = self.service_switch.get_characteristic(CHAR_ON)
|
||||
self.char_on.value = False
|
||||
self.char_on.setter_callback = self.set_state
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_state(new_state=state)
|
||||
|
||||
async_track_state_change(self._hass, self._entity_id,
|
||||
self.update_state)
|
||||
|
||||
def set_state(self, value):
|
||||
"""Move switch state to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set switch state to %s",
|
||||
self._entity_id, value)
|
||||
self.flag_target_state = True
|
||||
service = 'turn_on' if value else 'turn_off'
|
||||
self._hass.services.call(self._domain, service,
|
||||
{ATTR_ENTITY_ID: self._entity_id})
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update switch state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
current_state = (new_state.state == 'on')
|
||||
if not self.flag_target_state:
|
||||
_LOGGER.debug("%s: Set current state to %s",
|
||||
self._entity_id, current_state)
|
||||
self.char_on.set_value(current_state, should_callback=False)
|
||||
else:
|
||||
self.flag_target_state = False
|
||||
73
homeassistant/components/homekit/type_covers.py
Normal file
73
homeassistant/components/homekit/type_covers.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Class to hold all cover accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import ATTR_CURRENT_POSITION
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import (
|
||||
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
|
||||
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('WindowCovering')
|
||||
class WindowCovering(HomeAccessory):
|
||||
"""Generate a Window accessory for a cover entity.
|
||||
|
||||
The cover entity must support: set_cover_position.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, **kwargs):
|
||||
"""Initialize a WindowCovering accessory object."""
|
||||
super().__init__(display_name, entity_id,
|
||||
CATEGORY_WINDOW_COVERING, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
|
||||
self.current_position = None
|
||||
self.homekit_target = None
|
||||
|
||||
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
|
||||
self.char_current_position = serv_cover. \
|
||||
get_characteristic(CHAR_CURRENT_POSITION)
|
||||
self.char_target_position = serv_cover. \
|
||||
get_characteristic(CHAR_TARGET_POSITION)
|
||||
self.char_position_state = serv_cover. \
|
||||
get_characteristic(CHAR_POSITION_STATE)
|
||||
self.char_current_position.value = 0
|
||||
self.char_target_position.value = 0
|
||||
self.char_position_state.value = 0
|
||||
|
||||
self.char_target_position.setter_callback = self.move_cover
|
||||
|
||||
def move_cover(self, value):
|
||||
"""Move cover to value if call came from HomeKit."""
|
||||
self.char_target_position.set_value(value, should_callback=False)
|
||||
if value != self.current_position:
|
||||
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
|
||||
self.homekit_target = value
|
||||
if value > self.current_position:
|
||||
self.char_position_state.set_value(1)
|
||||
elif value < self.current_position:
|
||||
self.char_position_state.set_value(0)
|
||||
self.hass.components.cover.set_cover_position(
|
||||
value, self.entity_id)
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update cover position after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if isinstance(current_position, int):
|
||||
self.current_position = current_position
|
||||
self.char_current_position.set_value(self.current_position)
|
||||
if self.homekit_target is None or \
|
||||
abs(self.current_position - self.homekit_target) < 6:
|
||||
self.char_target_position.set_value(self.current_position)
|
||||
self.char_position_state.set_value(2)
|
||||
self.homekit_target = None
|
||||
187
homeassistant/components/homekit/type_lights.py
Normal file
187
homeassistant/components/homekit/type_lights.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""Class to hold all light accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
|
||||
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import (
|
||||
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
|
||||
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
RGB_COLOR = 'rgb_color'
|
||||
|
||||
|
||||
@TYPES.register('Light')
|
||||
class Light(HomeAccessory):
|
||||
"""Generate a Light accessory for a light entity.
|
||||
|
||||
Currently supports: state, brightness, color temperature, rgb_color.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, name, **kwargs):
|
||||
"""Initialize a new Light accessory object."""
|
||||
super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
|
||||
CHAR_HUE: False, CHAR_SATURATION: False,
|
||||
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
|
||||
self._state = 0
|
||||
|
||||
self.chars = []
|
||||
self._features = self.hass.states.get(self.entity_id) \
|
||||
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
if self._features & SUPPORT_BRIGHTNESS:
|
||||
self.chars.append(CHAR_BRIGHTNESS)
|
||||
if self._features & SUPPORT_COLOR_TEMP:
|
||||
self.chars.append(CHAR_COLOR_TEMPERATURE)
|
||||
if self._features & SUPPORT_COLOR:
|
||||
self.chars.append(CHAR_HUE)
|
||||
self.chars.append(CHAR_SATURATION)
|
||||
self._hue = None
|
||||
self._saturation = None
|
||||
|
||||
serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars)
|
||||
self.char_on = serv_light.get_characteristic(CHAR_ON)
|
||||
self.char_on.setter_callback = self.set_state
|
||||
self.char_on.value = self._state
|
||||
|
||||
if CHAR_BRIGHTNESS in self.chars:
|
||||
self.char_brightness = serv_light \
|
||||
.get_characteristic(CHAR_BRIGHTNESS)
|
||||
self.char_brightness.setter_callback = self.set_brightness
|
||||
self.char_brightness.value = 0
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
self.char_color_temperature = serv_light \
|
||||
.get_characteristic(CHAR_COLOR_TEMPERATURE)
|
||||
self.char_color_temperature.setter_callback = \
|
||||
self.set_color_temperature
|
||||
min_mireds = self.hass.states.get(self.entity_id) \
|
||||
.attributes.get(ATTR_MIN_MIREDS, 153)
|
||||
max_mireds = self.hass.states.get(self.entity_id) \
|
||||
.attributes.get(ATTR_MAX_MIREDS, 500)
|
||||
self.char_color_temperature.override_properties({
|
||||
'minValue': min_mireds, 'maxValue': max_mireds})
|
||||
self.char_color_temperature.value = min_mireds
|
||||
if CHAR_HUE in self.chars:
|
||||
self.char_hue = serv_light.get_characteristic(CHAR_HUE)
|
||||
self.char_hue.setter_callback = self.set_hue
|
||||
self.char_hue.value = 0
|
||||
if CHAR_SATURATION in self.chars:
|
||||
self.char_saturation = serv_light \
|
||||
.get_characteristic(CHAR_SATURATION)
|
||||
self.char_saturation.setter_callback = self.set_saturation
|
||||
self.char_saturation.value = 75
|
||||
|
||||
def set_state(self, value):
|
||||
"""Set state if call came from HomeKit."""
|
||||
if self._state == value:
|
||||
return
|
||||
|
||||
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
|
||||
self._flag[CHAR_ON] = True
|
||||
self.char_on.set_value(value, should_callback=False)
|
||||
|
||||
if value == 1:
|
||||
self.hass.components.light.turn_on(self.entity_id)
|
||||
elif value == 0:
|
||||
self.hass.components.light.turn_off(self.entity_id)
|
||||
|
||||
def set_brightness(self, value):
|
||||
"""Set brightness if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
|
||||
self._flag[CHAR_BRIGHTNESS] = True
|
||||
self.char_brightness.set_value(value, should_callback=False)
|
||||
if value != 0:
|
||||
self.hass.components.light.turn_on(
|
||||
self.entity_id, brightness_pct=value)
|
||||
else:
|
||||
self.hass.components.light.turn_off(self.entity_id)
|
||||
|
||||
def set_color_temperature(self, value):
|
||||
"""Set color temperature if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
|
||||
self._flag[CHAR_COLOR_TEMPERATURE] = True
|
||||
self.char_color_temperature.set_value(value, should_callback=False)
|
||||
self.hass.components.light.turn_on(self.entity_id, color_temp=value)
|
||||
|
||||
def set_saturation(self, value):
|
||||
"""Set saturation if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set saturation to %d', self.entity_id, value)
|
||||
self._flag[CHAR_SATURATION] = True
|
||||
self.char_saturation.set_value(value, should_callback=False)
|
||||
self._saturation = value
|
||||
self.set_color()
|
||||
|
||||
def set_hue(self, value):
|
||||
"""Set hue if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set hue to %d', self.entity_id, value)
|
||||
self._flag[CHAR_HUE] = True
|
||||
self.char_hue.set_value(value, should_callback=False)
|
||||
self._hue = value
|
||||
self.set_color()
|
||||
|
||||
def set_color(self):
|
||||
"""Set color if call came from HomeKit."""
|
||||
# Handle Color
|
||||
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
|
||||
self._flag[CHAR_SATURATION]:
|
||||
color = (self._hue, self._saturation)
|
||||
_LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color)
|
||||
self._flag.update({
|
||||
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
|
||||
self.hass.components.light.turn_on(
|
||||
self.entity_id, hs_color=color)
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update light after state change."""
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
# Handle State
|
||||
state = new_state.state
|
||||
if state in (STATE_ON, STATE_OFF):
|
||||
self._state = 1 if state == STATE_ON else 0
|
||||
if not self._flag[CHAR_ON] and self.char_on.value != self._state:
|
||||
self.char_on.set_value(self._state, should_callback=False)
|
||||
self._flag[CHAR_ON] = False
|
||||
|
||||
# Handle Brightness
|
||||
if CHAR_BRIGHTNESS in self.chars:
|
||||
brightness = new_state.attributes.get(ATTR_BRIGHTNESS)
|
||||
if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int):
|
||||
brightness = round(brightness / 255 * 100, 0)
|
||||
if self.char_brightness.value != brightness:
|
||||
self.char_brightness.set_value(brightness,
|
||||
should_callback=False)
|
||||
self._flag[CHAR_BRIGHTNESS] = False
|
||||
|
||||
# Handle color temperature
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
|
||||
if not self._flag[CHAR_COLOR_TEMPERATURE] \
|
||||
and isinstance(color_temperature, int):
|
||||
self.char_color_temperature.set_value(color_temperature,
|
||||
should_callback=False)
|
||||
self._flag[CHAR_COLOR_TEMPERATURE] = False
|
||||
|
||||
# Handle Color
|
||||
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
|
||||
hue, saturation = new_state.attributes.get(
|
||||
ATTR_HS_COLOR, (None, None))
|
||||
if not self._flag[RGB_COLOR] and (
|
||||
hue != self._hue or saturation != self._saturation) and \
|
||||
isinstance(hue, (int, float)) and \
|
||||
isinstance(saturation, (int, float)):
|
||||
self.char_hue.set_value(hue, should_callback=False)
|
||||
self.char_saturation.set_value(saturation,
|
||||
should_callback=False)
|
||||
self._hue, self._saturation = (hue, saturation)
|
||||
self._flag[RGB_COLOR] = False
|
||||
@@ -5,13 +5,12 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
ATTR_ENTITY_ID, ATTR_CODE)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import (
|
||||
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
|
||||
CHAR_TARGET_SECURITY_STATE)
|
||||
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
|
||||
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,49 +27,42 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
|
||||
class SecuritySystem(HomeAccessory):
|
||||
"""Generate an SecuritySystem accessory for an alarm control panel."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, alarm_code=None):
|
||||
def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs):
|
||||
"""Initialize a SecuritySystem accessory object."""
|
||||
super().__init__(display_name, entity_id, 'ALARM_SYSTEM')
|
||||
super().__init__(display_name, entity_id,
|
||||
CATEGORY_ALARM_SYSTEM, **kwargs)
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
self._alarm_code = alarm_code
|
||||
|
||||
self.flag_target_state = False
|
||||
|
||||
self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
|
||||
self.char_current_state = self.service_alarm. \
|
||||
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
|
||||
self.char_current_state = serv_alarm. \
|
||||
get_characteristic(CHAR_CURRENT_SECURITY_STATE)
|
||||
self.char_current_state.value = 3
|
||||
self.char_target_state = self.service_alarm. \
|
||||
self.char_target_state = serv_alarm. \
|
||||
get_characteristic(CHAR_TARGET_SECURITY_STATE)
|
||||
self.char_target_state.value = 3
|
||||
|
||||
self.char_target_state.setter_callback = self.set_security_state
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_security_state(new_state=state)
|
||||
|
||||
async_track_state_change(self._hass, self._entity_id,
|
||||
self.update_security_state)
|
||||
|
||||
def set_security_state(self, value):
|
||||
"""Move security state to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set security state to %d",
|
||||
self._entity_id, value)
|
||||
_LOGGER.debug('%s: Set security state to %d',
|
||||
self.entity_id, value)
|
||||
self.flag_target_state = True
|
||||
self.char_target_state.set_value(value, should_callback=False)
|
||||
hass_value = HOMEKIT_TO_HASS[value]
|
||||
service = STATE_TO_SERVICE[hass_value]
|
||||
|
||||
params = {ATTR_ENTITY_ID: self._entity_id}
|
||||
if self._alarm_code is not None:
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
if self._alarm_code:
|
||||
params[ATTR_CODE] = self._alarm_code
|
||||
self._hass.services.call('alarm_control_panel', service, params)
|
||||
self.hass.services.call('alarm_control_panel', service, params)
|
||||
|
||||
def update_security_state(self, entity_id=None,
|
||||
old_state=None, new_state=None):
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update security state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
@@ -78,15 +70,15 @@ class SecuritySystem(HomeAccessory):
|
||||
hass_state = new_state.state
|
||||
if hass_state not in HASS_TO_HOMEKIT:
|
||||
return
|
||||
|
||||
current_security_state = HASS_TO_HOMEKIT[hass_state]
|
||||
self.char_current_state.set_value(current_security_state)
|
||||
_LOGGER.debug("%s: Updated current state to %s (%d)",
|
||||
self._entity_id, hass_state,
|
||||
current_security_state)
|
||||
self.char_current_state.set_value(current_security_state,
|
||||
should_callback=False)
|
||||
_LOGGER.debug('%s: Updated current state to %s (%d)',
|
||||
self.entity_id, hass_state, current_security_state)
|
||||
|
||||
if not self.flag_target_state:
|
||||
self.char_target_state.set_value(current_security_state,
|
||||
should_callback=False)
|
||||
elif self.char_target_state.get_value() \
|
||||
== self.char_current_state.get_value():
|
||||
if self.char_target_state.value == self.char_current_state.value:
|
||||
self.flag_target_state = False
|
||||
77
homeassistant/components/homekit/type_sensors.py
Normal file
77
homeassistant/components/homekit/type_sensors.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Class to hold all sensor accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import (
|
||||
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
|
||||
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
|
||||
from .util import convert_to_float, temperature_to_homekit
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('TemperatureSensor')
|
||||
class TemperatureSensor(HomeAccessory):
|
||||
"""Generate a TemperatureSensor accessory for a temperature sensor.
|
||||
|
||||
Sensor entity must return temperature in °C, °F.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, name, **kwargs):
|
||||
"""Initialize a TemperatureSensor accessory object."""
|
||||
super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
|
||||
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
|
||||
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
||||
self.char_temp.override_properties(properties=PROP_CELSIUS)
|
||||
self.char_temp.value = 0
|
||||
self.unit = None
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update temperature after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
|
||||
temperature = convert_to_float(new_state.state)
|
||||
if temperature:
|
||||
temperature = temperature_to_homekit(temperature, unit)
|
||||
self.char_temp.set_value(temperature, should_callback=False)
|
||||
_LOGGER.debug('%s: Current temperature set to %d°C',
|
||||
self.entity_id, temperature)
|
||||
|
||||
|
||||
@TYPES.register('HumiditySensor')
|
||||
class HumiditySensor(HomeAccessory):
|
||||
"""Generate a HumiditySensor accessory as humidity sensor."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, *args, **kwargs):
|
||||
"""Initialize a HumiditySensor accessory object."""
|
||||
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
|
||||
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
|
||||
self.char_humidity = serv_humidity \
|
||||
.get_characteristic(CHAR_CURRENT_HUMIDITY)
|
||||
self.char_humidity.value = 0
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update accessory after state change."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
humidity = convert_to_float(new_state.state)
|
||||
if humidity:
|
||||
self.char_humidity.set_value(humidity, should_callback=False)
|
||||
_LOGGER.debug('%s: Percent set to %d%%',
|
||||
self.entity_id, humidity)
|
||||
55
homeassistant/components/homekit/type_switches.py
Normal file
55
homeassistant/components/homekit/type_switches.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Class to hold all switch accessories."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON)
|
||||
from homeassistant.core import split_entity_id
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('Switch')
|
||||
class Switch(HomeAccessory):
|
||||
"""Generate a Switch accessory."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, **kwargs):
|
||||
"""Initialize a Switch accessory object to represent a remote."""
|
||||
super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs)
|
||||
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
self._domain = split_entity_id(entity_id)[0]
|
||||
|
||||
self.flag_target_state = False
|
||||
|
||||
serv_switch = add_preload_service(self, SERV_SWITCH)
|
||||
self.char_on = serv_switch.get_characteristic(CHAR_ON)
|
||||
self.char_on.value = False
|
||||
self.char_on.setter_callback = self.set_state
|
||||
|
||||
def set_state(self, value):
|
||||
"""Move switch state to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set switch state to %s',
|
||||
self.entity_id, value)
|
||||
self.flag_target_state = True
|
||||
self.char_on.set_value(value, should_callback=False)
|
||||
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
|
||||
self.hass.services.call(self._domain, service,
|
||||
{ATTR_ENTITY_ID: self.entity_id})
|
||||
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update switch state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
current_state = (new_state.state == STATE_ON)
|
||||
if not self.flag_target_state:
|
||||
_LOGGER.debug('%s: Set current state to %s',
|
||||
self.entity_id, current_state)
|
||||
self.char_on.set_value(current_state, should_callback=False)
|
||||
|
||||
self.flag_target_state = False
|
||||
@@ -7,21 +7,19 @@ from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
||||
STATE_HEAT, STATE_COOL, STATE_AUTO)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
from .const import (
|
||||
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
|
||||
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
|
||||
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
|
||||
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||
from .util import temperature_to_homekit, temperature_to_states
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_OFF = 'off'
|
||||
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
|
||||
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
|
||||
HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
|
||||
@@ -33,61 +31,62 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
|
||||
class Thermostat(HomeAccessory):
|
||||
"""Generate a Thermostat accessory for a climate."""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name, support_auto=False):
|
||||
def __init__(self, hass, entity_id, display_name, support_auto, **kwargs):
|
||||
"""Initialize a Thermostat accessory object."""
|
||||
super().__init__(display_name, entity_id, 'THERMOSTAT')
|
||||
super().__init__(display_name, entity_id,
|
||||
CATEGORY_THERMOSTAT, **kwargs)
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self.hass = hass
|
||||
self.entity_id = entity_id
|
||||
self._call_timer = None
|
||||
self._unit = TEMP_CELSIUS
|
||||
|
||||
self.heat_cool_flag_target_state = False
|
||||
self.temperature_flag_target_state = False
|
||||
self.coolingthresh_flag_target_state = False
|
||||
self.heatingthresh_flag_target_state = False
|
||||
|
||||
extra_chars = None
|
||||
# Add additional characteristics if auto mode is supported
|
||||
if support_auto:
|
||||
extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE]
|
||||
extra_chars = [
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None
|
||||
|
||||
# Preload the thermostat service
|
||||
self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT,
|
||||
extra_chars)
|
||||
serv_thermostat = add_preload_service(self, SERV_THERMOSTAT,
|
||||
extra_chars)
|
||||
|
||||
# Current and target mode characteristics
|
||||
self.char_current_heat_cool = self.service_thermostat. \
|
||||
self.char_current_heat_cool = serv_thermostat. \
|
||||
get_characteristic(CHAR_CURRENT_HEATING_COOLING)
|
||||
self.char_current_heat_cool.value = 0
|
||||
self.char_target_heat_cool = self.service_thermostat. \
|
||||
self.char_target_heat_cool = serv_thermostat. \
|
||||
get_characteristic(CHAR_TARGET_HEATING_COOLING)
|
||||
self.char_target_heat_cool.value = 0
|
||||
self.char_target_heat_cool.setter_callback = self.set_heat_cool
|
||||
|
||||
# Current and target temperature characteristics
|
||||
self.char_current_temp = self.service_thermostat. \
|
||||
self.char_current_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
||||
self.char_current_temp.value = 21.0
|
||||
self.char_target_temp = self.service_thermostat. \
|
||||
self.char_target_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_TARGET_TEMPERATURE)
|
||||
self.char_target_temp.value = 21.0
|
||||
self.char_target_temp.setter_callback = self.set_target_temperature
|
||||
|
||||
# Display units characteristic
|
||||
self.char_display_units = self.service_thermostat. \
|
||||
self.char_display_units = serv_thermostat. \
|
||||
get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
|
||||
self.char_display_units.value = 0
|
||||
|
||||
# If the device supports it: high and low temperature characteristics
|
||||
if support_auto:
|
||||
self.char_cooling_thresh_temp = self.service_thermostat. \
|
||||
self.char_cooling_thresh_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE)
|
||||
self.char_cooling_thresh_temp.value = 23.0
|
||||
self.char_cooling_thresh_temp.setter_callback = \
|
||||
self.set_cooling_threshold
|
||||
|
||||
self.char_heating_thresh_temp = self.service_thermostat. \
|
||||
self.char_heating_thresh_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||
self.char_heating_thresh_temp.value = 19.0
|
||||
self.char_heating_thresh_temp.setter_callback = \
|
||||
@@ -96,132 +95,127 @@ class Thermostat(HomeAccessory):
|
||||
self.char_cooling_thresh_temp = None
|
||||
self.char_heating_thresh_temp = None
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_thermostat(new_state=state)
|
||||
|
||||
async_track_state_change(self._hass, self._entity_id,
|
||||
self.update_thermostat)
|
||||
|
||||
def set_heat_cool(self, value):
|
||||
"""Move operation mode to value if call came from HomeKit."""
|
||||
self.char_target_heat_cool.set_value(value, should_callback=False)
|
||||
if value in HC_HOMEKIT_TO_HASS:
|
||||
_LOGGER.debug("%s: Set heat-cool to %d", self._entity_id, value)
|
||||
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
|
||||
self.heat_cool_flag_target_state = True
|
||||
hass_value = HC_HOMEKIT_TO_HASS[value]
|
||||
self._hass.services.call('climate', 'set_operation_mode',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_OPERATION_MODE: hass_value})
|
||||
self.hass.components.climate.set_operation_mode(
|
||||
operation_mode=hass_value, entity_id=self.entity_id)
|
||||
|
||||
def set_cooling_threshold(self, value):
|
||||
"""Set cooling threshold temp to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set cooling threshold temperature to %.2f",
|
||||
self._entity_id, value)
|
||||
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
|
||||
self.entity_id, value)
|
||||
self.coolingthresh_flag_target_state = True
|
||||
low = self.char_heating_thresh_temp.get_value()
|
||||
self._hass.services.call(
|
||||
'climate', 'set_temperature',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_TARGET_TEMP_HIGH: value,
|
||||
ATTR_TARGET_TEMP_LOW: low})
|
||||
self.char_cooling_thresh_temp.set_value(value, should_callback=False)
|
||||
low = self.char_heating_thresh_temp.value
|
||||
low = temperature_to_states(low, self._unit)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self.hass.components.climate.set_temperature(
|
||||
entity_id=self.entity_id, target_temp_high=value,
|
||||
target_temp_low=low)
|
||||
|
||||
def set_heating_threshold(self, value):
|
||||
"""Set heating threshold temp to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set heating threshold temperature to %.2f",
|
||||
self._entity_id, value)
|
||||
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
|
||||
self.entity_id, value)
|
||||
self.heatingthresh_flag_target_state = True
|
||||
self.char_heating_thresh_temp.set_value(value, should_callback=False)
|
||||
# Home assistant always wants to set low and high at the same time
|
||||
high = self.char_cooling_thresh_temp.get_value()
|
||||
self._hass.services.call(
|
||||
'climate', 'set_temperature',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_TARGET_TEMP_LOW: value,
|
||||
ATTR_TARGET_TEMP_HIGH: high})
|
||||
high = self.char_cooling_thresh_temp.value
|
||||
high = temperature_to_states(high, self._unit)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self.hass.components.climate.set_temperature(
|
||||
entity_id=self.entity_id, target_temp_high=high,
|
||||
target_temp_low=value)
|
||||
|
||||
def set_target_temperature(self, value):
|
||||
"""Set target temperature to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set target temperature to %.2f",
|
||||
self._entity_id, value)
|
||||
_LOGGER.debug('%s: Set target temperature to %.2f°C',
|
||||
self.entity_id, value)
|
||||
self.temperature_flag_target_state = True
|
||||
self._hass.services.call(
|
||||
'climate', 'set_temperature',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_TEMPERATURE: value})
|
||||
self.char_target_temp.set_value(value, should_callback=False)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self.hass.components.climate.set_temperature(
|
||||
temperature=value, entity_id=self.entity_id)
|
||||
|
||||
def update_thermostat(self, entity_id=None,
|
||||
old_state=None, new_state=None):
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update security state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
|
||||
TEMP_CELSIUS)
|
||||
|
||||
# Update current temperature
|
||||
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
|
||||
if current_temp is not None:
|
||||
if isinstance(current_temp, (int, float)):
|
||||
current_temp = temperature_to_homekit(current_temp, self._unit)
|
||||
self.char_current_temp.set_value(current_temp)
|
||||
|
||||
# Update target temperature
|
||||
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
|
||||
if target_temp is not None:
|
||||
if isinstance(target_temp, (int, float)):
|
||||
target_temp = temperature_to_homekit(target_temp, self._unit)
|
||||
if not self.temperature_flag_target_state:
|
||||
self.char_target_temp.set_value(target_temp,
|
||||
should_callback=False)
|
||||
else:
|
||||
self.temperature_flag_target_state = False
|
||||
self.temperature_flag_target_state = False
|
||||
|
||||
# Update cooling threshold temperature if characteristic exists
|
||||
if self.char_cooling_thresh_temp is not None:
|
||||
if self.char_cooling_thresh_temp:
|
||||
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if cooling_thresh is not None:
|
||||
if isinstance(cooling_thresh, (int, float)):
|
||||
cooling_thresh = temperature_to_homekit(cooling_thresh,
|
||||
self._unit)
|
||||
if not self.coolingthresh_flag_target_state:
|
||||
self.char_cooling_thresh_temp.set_value(
|
||||
cooling_thresh, should_callback=False)
|
||||
else:
|
||||
self.coolingthresh_flag_target_state = False
|
||||
self.coolingthresh_flag_target_state = False
|
||||
|
||||
# Update heating threshold temperature if characteristic exists
|
||||
if self.char_heating_thresh_temp is not None:
|
||||
if self.char_heating_thresh_temp:
|
||||
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
|
||||
if heating_thresh is not None:
|
||||
if isinstance(heating_thresh, (int, float)):
|
||||
heating_thresh = temperature_to_homekit(heating_thresh,
|
||||
self._unit)
|
||||
if not self.heatingthresh_flag_target_state:
|
||||
self.char_heating_thresh_temp.set_value(
|
||||
heating_thresh, should_callback=False)
|
||||
else:
|
||||
self.heatingthresh_flag_target_state = False
|
||||
self.heatingthresh_flag_target_state = False
|
||||
|
||||
# Update display units
|
||||
display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if display_units is not None \
|
||||
and display_units in UNIT_HASS_TO_HOMEKIT:
|
||||
self.char_display_units.set_value(
|
||||
UNIT_HASS_TO_HOMEKIT[display_units])
|
||||
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
|
||||
self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit])
|
||||
|
||||
# Update target operation mode
|
||||
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
|
||||
if operation_mode is not None \
|
||||
if operation_mode \
|
||||
and operation_mode in HC_HASS_TO_HOMEKIT:
|
||||
if not self.heat_cool_flag_target_state:
|
||||
self.char_target_heat_cool.set_value(
|
||||
HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False)
|
||||
else:
|
||||
self.heat_cool_flag_target_state = False
|
||||
self.heat_cool_flag_target_state = False
|
||||
|
||||
# Set current operation mode based on temperatures and target mode
|
||||
if operation_mode == STATE_HEAT:
|
||||
if current_temp < target_temp:
|
||||
if isinstance(target_temp, float) and current_temp < target_temp:
|
||||
current_operation_mode = STATE_HEAT
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
elif operation_mode == STATE_COOL:
|
||||
if current_temp > target_temp:
|
||||
if isinstance(target_temp, float) and current_temp > target_temp:
|
||||
current_operation_mode = STATE_COOL
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
elif operation_mode == STATE_AUTO:
|
||||
# Check if auto is supported
|
||||
if self.char_cooling_thresh_temp is not None:
|
||||
lower_temp = self.char_heating_thresh_temp.get_value()
|
||||
upper_temp = self.char_cooling_thresh_temp.get_value()
|
||||
if self.char_cooling_thresh_temp:
|
||||
lower_temp = self.char_heating_thresh_temp.value
|
||||
upper_temp = self.char_cooling_thresh_temp.value
|
||||
if current_temp < lower_temp:
|
||||
current_operation_mode = STATE_HEAT
|
||||
elif current_temp > upper_temp:
|
||||
@@ -232,9 +226,11 @@ class Thermostat(HomeAccessory):
|
||||
# Check if heating or cooling are supported
|
||||
heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST]
|
||||
cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST]
|
||||
if current_temp < target_temp and heat:
|
||||
if isinstance(target_temp, float) and \
|
||||
current_temp < target_temp and heat:
|
||||
current_operation_mode = STATE_HEAT
|
||||
elif current_temp > target_temp and cool:
|
||||
elif isinstance(target_temp, float) and \
|
||||
current_temp > target_temp and cool:
|
||||
current_operation_mode = STATE_COOL
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
66
homeassistant/components/homekit/util.py
Normal file
66
homeassistant/components/homekit/util.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Collection of useful functions for the HomeKit component."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, TEMP_CELSIUS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.temperature as temp_util
|
||||
from .const import HOMEKIT_NOTIFY_ID
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_entity_config(values):
|
||||
"""Validate config entry for CONF_ENTITY."""
|
||||
entities = {}
|
||||
for key, config in values.items():
|
||||
entity = cv.entity_id(key)
|
||||
params = {}
|
||||
if not isinstance(config, dict):
|
||||
raise vol.Invalid('The configuration for "{}" must be '
|
||||
' an dictionary.'.format(entity))
|
||||
|
||||
domain, _ = split_entity_id(entity)
|
||||
|
||||
if domain == 'alarm_control_panel':
|
||||
code = config.get(ATTR_CODE)
|
||||
params[ATTR_CODE] = cv.string(code) if code else None
|
||||
|
||||
entities[entity] = params
|
||||
return entities
|
||||
|
||||
|
||||
def show_setup_message(bridge, hass):
|
||||
"""Display persistent notification with setup information."""
|
||||
pin = bridge.pincode.decode()
|
||||
_LOGGER.info('Pincode: %s', pin)
|
||||
message = 'To setup Home Assistant in the Home App, enter the ' \
|
||||
'following code:\n### {}'.format(pin)
|
||||
hass.components.persistent_notification.create(
|
||||
message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID)
|
||||
|
||||
|
||||
def dismiss_setup_message(hass):
|
||||
"""Dismiss persistent notification and remove QR code."""
|
||||
hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID)
|
||||
|
||||
|
||||
def convert_to_float(state):
|
||||
"""Return float of state, catch errors."""
|
||||
try:
|
||||
return float(state)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def temperature_to_homekit(temperature, unit):
|
||||
"""Convert temperature to Celsius for HomeKit."""
|
||||
return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1)
|
||||
|
||||
|
||||
def temperature_to_states(temperature, unit):
|
||||
"""Convert temperature back from Celsius to Home Assistant unit."""
|
||||
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1)
|
||||
@@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.39']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.40']
|
||||
DOMAIN = 'homematic'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor'
|
||||
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
|
||||
DISCOVER_COVER = 'homematic.cover'
|
||||
DISCOVER_CLIMATE = 'homematic.climate'
|
||||
DISCOVER_LOCKS = 'homematic.locks'
|
||||
|
||||
ATTR_DISCOVER_DEVICES = 'devices'
|
||||
ATTR_PARAM = 'param'
|
||||
@@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode'
|
||||
HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: [
|
||||
'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren',
|
||||
'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'],
|
||||
'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'],
|
||||
DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'],
|
||||
DISCOVER_SENSORS: [
|
||||
'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP',
|
||||
@@ -68,7 +69,7 @@ HM_DEVICE_TYPES = {
|
||||
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
|
||||
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
|
||||
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
|
||||
'IPSmoke', 'RFSiren', 'PresenceIP'],
|
||||
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'],
|
||||
DISCOVER_CLIMATE: [
|
||||
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
|
||||
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
|
||||
@@ -78,7 +79,8 @@ HM_DEVICE_TYPES = {
|
||||
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
|
||||
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
|
||||
'WiredSensor', 'PresenceIP'],
|
||||
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt']
|
||||
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
|
||||
DISCOVER_LOCKS: ['KeyMatic']
|
||||
}
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE = [
|
||||
@@ -86,6 +88,10 @@ HM_IGNORE_DISCOVERY_NODE = [
|
||||
'ACTUAL_HUMIDITY'
|
||||
]
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
|
||||
'ACTUAL_TEMPERATURE': ['IPAreaThermostat'],
|
||||
}
|
||||
|
||||
HM_ATTRIBUTE_SUPPORT = {
|
||||
'LOWBAT': ['battery', {0: 'High', 1: 'Low'}],
|
||||
'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}],
|
||||
@@ -460,7 +466,8 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
('cover', DISCOVER_COVER),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS),
|
||||
('sensor', DISCOVER_SENSORS),
|
||||
('climate', DISCOVER_CLIMATE)):
|
||||
('climate', DISCOVER_CLIMATE),
|
||||
('lock', DISCOVER_LOCKS)):
|
||||
# Get all devices of a specific type
|
||||
found_devices = _get_devices(
|
||||
hass, discovery_type, addresses, interface)
|
||||
@@ -505,7 +512,8 @@ def _get_devices(hass, discovery_type, keys, interface):
|
||||
|
||||
# Generate options for 1...n elements with 1...n parameters
|
||||
for param, channels in metadata.items():
|
||||
if param in HM_IGNORE_DISCOVERY_NODE:
|
||||
if param in HM_IGNORE_DISCOVERY_NODE and class_name not in \
|
||||
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS.get(param, []):
|
||||
continue
|
||||
|
||||
# Add devices
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user