Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c526fcd40f | ||
|
|
b61197196e | ||
|
|
e0a1b87296 | ||
|
|
d3ed6d5242 | ||
|
|
5d6455415e | ||
|
|
a27b37be59 | ||
|
|
f6a67d99e4 | ||
|
|
486263fff7 | ||
|
|
5a469f4d4b | ||
|
|
889eef78e4 | ||
|
|
794cfb7976 | ||
|
|
9055922153 | ||
|
|
2330268842 | ||
|
|
e81d17dae0 | ||
|
|
5820bab9b1 | ||
|
|
3a00c3de64 | ||
|
|
79240a0d47 | ||
|
|
c03d04d826 | ||
|
|
432304be82 | ||
|
|
294d8171a2 | ||
|
|
a46ddcf6dd | ||
|
|
5ca006cc9c | ||
|
|
564ed26aeb | ||
|
|
5860267410 | ||
|
|
ec9638f4d1 | ||
|
|
7db8bbf385 | ||
|
|
f4d7bbd044 | ||
|
|
ca81180e6d | ||
|
|
de4c8adca2 | ||
|
|
823e260c2a | ||
|
|
1c8b5838cd | ||
|
|
3473ef63af | ||
|
|
2cced1dac3 | ||
|
|
b5d3a4736b | ||
|
|
e627544479 | ||
|
|
d547345f90 | ||
|
|
4ec3289f9c | ||
|
|
638dd25aff | ||
|
|
37efd5a5cd | ||
|
|
168065b9bc | ||
|
|
95cd2035b6 | ||
|
|
aeba81e193 | ||
|
|
c7e327ea87 | ||
|
|
ed06b8cead | ||
|
|
a79c7ee217 | ||
|
|
6bf23f9167 | ||
|
|
1b3963299d | ||
|
|
c461a7c7e2 | ||
|
|
0cfff13be1 | ||
|
|
0245189670 | ||
|
|
7777d5811f | ||
|
|
7259cc878e | ||
|
|
a4214afddb | ||
|
|
b078f6c342 | ||
|
|
81974885ee | ||
|
|
b2c5a9f5fe | ||
|
|
04cb893d10 | ||
|
|
3b228c78c0 | ||
|
|
4e91e6d103 | ||
|
|
cb4e886a4f | ||
|
|
bee80c5b79 | ||
|
|
4479761131 | ||
|
|
20f1e1609f | ||
|
|
1f1115f631 | ||
|
|
f7c2ec19ef | ||
|
|
fed7bd9473 | ||
|
|
4d6070e33a | ||
|
|
0a7e6ac222 | ||
|
|
f892c3394b | ||
|
|
929d49ed6f | ||
|
|
3c1f8cd882 | ||
|
|
f21da7cfdc | ||
|
|
39d33c97ff | ||
|
|
c952f2e18a | ||
|
|
0fc7f37185 | ||
|
|
9cff6c7e6a | ||
|
|
e66268dffe | ||
|
|
c13b510ba3 | ||
|
|
5f4baa67dc | ||
|
|
1db7e2c9d6 | ||
|
|
fa324dce9c | ||
|
|
56c694b477 | ||
|
|
3f764f1981 | ||
|
|
63d6734612 | ||
|
|
f9743c29cd | ||
|
|
bdb7a29586 | ||
|
|
22c36f0ad3 | ||
|
|
fd6373c7aa | ||
|
|
87fe674c70 | ||
|
|
ddec566e10 | ||
|
|
454d8535f8 | ||
|
|
8e4942088e | ||
|
|
3af527b1b5 | ||
|
|
69d5738e47 | ||
|
|
379c10985b | ||
|
|
821cf7135d | ||
|
|
d986bdab98 | ||
|
|
53d9fd18b7 | ||
|
|
38a1f06d14 | ||
|
|
2e2d0f48fb | ||
|
|
4652b8aea1 | ||
|
|
ef1cbd3aea | ||
|
|
31cedf83c7 | ||
|
|
19a97580fc | ||
|
|
17f3cf0389 | ||
|
|
4e02300cbc | ||
|
|
015cdd155c | ||
|
|
d4e603cc6a | ||
|
|
7ae374e11f | ||
|
|
292b403dc3 | ||
|
|
b815898ddb | ||
|
|
b1855f1d1d | ||
|
|
bd6a17a3a5 | ||
|
|
29fad3fa3c | ||
|
|
0c43466225 | ||
|
|
0d6c95ac44 | ||
|
|
6776e942d7 | ||
|
|
879e32f670 | ||
|
|
3a246df544 | ||
|
|
9577525b0b | ||
|
|
6b410d8076 | ||
|
|
9e82433a3e | ||
|
|
8ceaa72ba3 | ||
|
|
fce994ea76 | ||
|
|
4390fed168 | ||
|
|
0f8e48c26d | ||
|
|
2d556486bf | ||
|
|
850a20a626 | ||
|
|
68dc0d4d99 | ||
|
|
58e66c947b | ||
|
|
29f47d58bc | ||
|
|
8947052405 | ||
|
|
475b7896e2 | ||
|
|
b2a2cb3fd8 | ||
|
|
462a438f89 | ||
|
|
4ebc52ab52 | ||
|
|
8afeef2f36 | ||
|
|
c2525782aa | ||
|
|
bc4de4e769 | ||
|
|
9f324205cb | ||
|
|
fff85ab392 | ||
|
|
29f4b73230 | ||
|
|
606fa34792 | ||
|
|
7b452208b6 | ||
|
|
493de295ac | ||
|
|
d2106c40e1 | ||
|
|
9a0a5b7867 | ||
|
|
d8003c4d87 | ||
|
|
f7380dc927 | ||
|
|
ea6ca9252c | ||
|
|
bfc61c268a | ||
|
|
1c227bc0d9 | ||
|
|
bb870a688d | ||
|
|
40a98d56fa | ||
|
|
373508693a | ||
|
|
59fa4f18e4 | ||
|
|
253d5aea6e | ||
|
|
99ea2c17a1 | ||
|
|
7ab15c0e79 | ||
|
|
4e4d4365a0 | ||
|
|
1f82bb033d | ||
|
|
cadd797200 | ||
|
|
6df5e712f7 | ||
|
|
282e37ef14 | ||
|
|
0668fba7bd | ||
|
|
27270b49b4 | ||
|
|
8c5d6ee9c3 | ||
|
|
934c19445d | ||
|
|
72251e0375 | ||
|
|
b1e2275b47 | ||
|
|
af1bde6619 | ||
|
|
2daea92379 | ||
|
|
6cd9ca018a | ||
|
|
eb282b3bb3 | ||
|
|
fe0a9529ed | ||
|
|
1b7a64412d | ||
|
|
a187bd5455 | ||
|
|
3e962808e6 | ||
|
|
3d5a9b5e91 | ||
|
|
ba43218a73 | ||
|
|
d8bf15a2f5 | ||
|
|
dbbbe1ceef | ||
|
|
2817f03378 | ||
|
|
fcc164c31e | ||
|
|
65d5b64d8d | ||
|
|
f6547ec157 | ||
|
|
61cddaa441 | ||
|
|
b03c024f74 | ||
|
|
1a7522a594 | ||
|
|
d0b9f08bf2 | ||
|
|
3dd49b2b95 | ||
|
|
3ef9c99003 | ||
|
|
47183ce02e | ||
|
|
f2dea4615f | ||
|
|
b4635db5ac | ||
|
|
b668b19543 | ||
|
|
cfb1853bbd | ||
|
|
b784d80973 | ||
|
|
2084ad2164 | ||
|
|
9c77f5f5a9 | ||
|
|
8a750eba68 | ||
|
|
5dbd554a10 | ||
|
|
d0296561f6 | ||
|
|
db212cfb00 | ||
|
|
6db5afe597 | ||
|
|
2ba83655bb | ||
|
|
6e27e73474 | ||
|
|
f0fe8cb2fe | ||
|
|
3d9f03d4f1 | ||
|
|
235707d31c | ||
|
|
8cb87d5e64 | ||
|
|
4cb0e4b3c2 | ||
|
|
2ba5f1f45e | ||
|
|
d7f9be9640 | ||
|
|
34f06e8eef | ||
|
|
efd45549e4 | ||
|
|
34a4db57db | ||
|
|
e62ef067cc | ||
|
|
df37cb11fa | ||
|
|
857d6b5b49 | ||
|
|
62a740ba22 | ||
|
|
a83e741dc7 | ||
|
|
7695ca2c8b | ||
|
|
3f5c748560 | ||
|
|
fb32cc39e1 | ||
|
|
b548116f9b | ||
|
|
2031b2803f | ||
|
|
50775ce509 | ||
|
|
709df1e844 | ||
|
|
09d826edf4 | ||
|
|
086f64b06c | ||
|
|
6ad62a2ccb | ||
|
|
92fe9aadc8 | ||
|
|
e1d1cf76ca | ||
|
|
1317297191 | ||
|
|
64a393b377 | ||
|
|
3ad64b0a66 | ||
|
|
2664ca498e | ||
|
|
5b44e83c0f | ||
|
|
b8b4e32758 | ||
|
|
2b60fca08d | ||
|
|
f43092c563 | ||
|
|
68d2076b56 | ||
|
|
be5f0fb3ac | ||
|
|
e9b691173a | ||
|
|
2a77883146 |
45
.coveragerc
@@ -11,6 +11,9 @@ omit =
|
||||
homeassistant/components/abode.py
|
||||
homeassistant/components/*/abode.py
|
||||
|
||||
homeassistant/components/ads/__init__.py
|
||||
homeassistant/components/*/ads.py
|
||||
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
@@ -53,6 +56,8 @@ omit =
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
homeassistant/components/dominos.py
|
||||
|
||||
homeassistant/components/doorbird.py
|
||||
homeassistant/components/*/doorbird.py
|
||||
|
||||
@@ -80,7 +85,10 @@ omit =
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/*/hdmi_cec.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/hive.py
|
||||
homeassistant/components/*/hive.py
|
||||
|
||||
homeassistant/components/homematic/__init__.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
@@ -182,6 +190,9 @@ omit =
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/tahoma.py
|
||||
homeassistant/components/*/tahoma.py
|
||||
|
||||
homeassistant/components/tellduslive.py
|
||||
homeassistant/components/*/tellduslive.py
|
||||
|
||||
@@ -253,8 +264,10 @@ omit =
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/egardia.py
|
||||
homeassistant/components/alarm_control_panel/ialarm.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
@@ -271,14 +284,16 @@ omit =
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/calendar/caldav.py
|
||||
homeassistant/components/calendar/todoist.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/canary.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/ring.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/camera/yi.py
|
||||
homeassistant/components/climate/ephember.py
|
||||
@@ -286,6 +301,7 @@ omit =
|
||||
homeassistant/components/climate/flexit.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/honeywell.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
@@ -323,10 +339,10 @@ omit =
|
||||
homeassistant/components/device_tracker/sky_hub.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tado.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tile.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
@@ -344,8 +360,8 @@ omit =
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/light/avion.py
|
||||
homeassistant/components/light/blinkt.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/flux_led.py
|
||||
@@ -356,8 +372,8 @@ omit =
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/rpi_gpio_pwm.py
|
||||
homeassistant/components/light/sensehat.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/tplink.py
|
||||
@@ -368,9 +384,9 @@ omit =
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/nello.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/media_extractor.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
@@ -412,11 +428,13 @@ omit =
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/ue_smart_radio.py
|
||||
homeassistant/components/media_player/vizio.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/media_player/yamaha_musiccast.py
|
||||
homeassistant/components/media_player/ziggo_mediabox_xl.py
|
||||
homeassistant/components/mycroft.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
@@ -426,7 +444,6 @@ omit =
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
@@ -465,6 +482,7 @@ omit =
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
homeassistant/components/sensor/alpha_vantage.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
@@ -475,8 +493,8 @@ omit =
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/crimereports.py
|
||||
@@ -504,6 +522,7 @@ omit =
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gearbest.py
|
||||
homeassistant/components/sensor/geizhals.py
|
||||
homeassistant/components/sensor/gitter.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
@@ -589,7 +608,6 @@ omit =
|
||||
homeassistant/components/sensor/worldtidesinfo.py
|
||||
homeassistant/components/sensor/worxlandroid.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/shiftr.py
|
||||
homeassistant/components/spc.py
|
||||
@@ -615,16 +633,19 @@ omit =
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/snmp.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/xiaomi_miio.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
homeassistant/components/tts/baidu.py
|
||||
homeassistant/components/tts/microsoft.py
|
||||
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/metoffice.py
|
||||
@@ -633,8 +654,6 @@ omit =
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/util.py
|
||||
homeassistant/components/vacuum/mqtt.py
|
||||
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
|
||||
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ensure Docker script files uses LF to support Docker for Windows.
|
||||
setup_docker_prereqs eol=lf
|
||||
/virtualization/Docker/scripts/* eol=lf
|
||||
2
.gitignore
vendored
@@ -96,4 +96,4 @@ docs/build
|
||||
desktop.ini
|
||||
/home-assistant.pyproj
|
||||
/home-assistant.sln
|
||||
/.vs/home-assistant/v14
|
||||
/.vs/*
|
||||
|
||||
@@ -8,18 +8,18 @@ matrix:
|
||||
include:
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=lint
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=pylint
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=py34
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5"
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
# - python: "3.6-dev"
|
||||
# env: TOXENV=py36
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
@@ -29,5 +29,5 @@ cache:
|
||||
- $HOME/.cache/pip
|
||||
install: pip install -U tox coveralls
|
||||
language: python
|
||||
script: travis_wait tox
|
||||
script: travis_wait 30 tox --develop
|
||||
after_success: coveralls
|
||||
|
||||
@@ -46,6 +46,7 @@ homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/light/tplink.py @rytilahti
|
||||
homeassistant/components/light/yeelight.py @rytilahti
|
||||
@@ -53,6 +54,7 @@ homeassistant/components/media_player/kodi.py @armills
|
||||
homeassistant/components/media_player/monoprice.py @etsinko
|
||||
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/gearbest.py @HerrHofrat
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
@@ -63,13 +65,19 @@ homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
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/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tradfri.py @ggravlingen
|
||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||
|
||||
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
@@ -230,7 +230,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
||||
|
||||
def cmdline() -> List[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if sys.argv[0].endswith(os.path.sep + '__main__.py'):
|
||||
if os.path.basename(sys.argv[0]) == '__main__.py':
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
|
||||
217
homeassistant/components/ads/__init__.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
ADS Component.
|
||||
|
||||
For more details about this component, please refer to the documentation.
|
||||
https://home-assistant.io/components/ads/
|
||||
|
||||
"""
|
||||
import os
|
||||
import threading
|
||||
import struct
|
||||
import logging
|
||||
import ctypes
|
||||
from collections import namedtuple
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
|
||||
EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyads==2.2.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_ADS = 'data_ads'
|
||||
|
||||
# Supported Types
|
||||
ADSTYPE_INT = 'int'
|
||||
ADSTYPE_UINT = 'uint'
|
||||
ADSTYPE_BYTE = 'byte'
|
||||
ADSTYPE_BOOL = 'bool'
|
||||
|
||||
DOMAIN = 'ads'
|
||||
|
||||
# config variable names
|
||||
CONF_ADS_VAR = 'adsvar'
|
||||
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
|
||||
CONF_ADS_TYPE = 'adstype'
|
||||
CONF_ADS_FACTOR = 'factor'
|
||||
CONF_ADS_VALUE = 'value'
|
||||
|
||||
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_IP_ADDRESS): cv.string,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT,
|
||||
ADSTYPE_BYTE]),
|
||||
vol.Required(CONF_ADS_VALUE): cv.match_all
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the ADS component."""
|
||||
import pyads
|
||||
conf = config[DOMAIN]
|
||||
|
||||
# get ads connection parameters from config
|
||||
net_id = conf.get(CONF_DEVICE)
|
||||
ip_address = conf.get(CONF_IP_ADDRESS)
|
||||
port = conf.get(CONF_PORT)
|
||||
|
||||
# create a new ads connection
|
||||
client = pyads.Connection(net_id, port, ip_address)
|
||||
|
||||
# add some constants to AdsHub
|
||||
AdsHub.ADS_TYPEMAP = {
|
||||
ADSTYPE_BOOL: pyads.PLCTYPE_BOOL,
|
||||
ADSTYPE_BYTE: pyads.PLCTYPE_BYTE,
|
||||
ADSTYPE_INT: pyads.PLCTYPE_INT,
|
||||
ADSTYPE_UINT: pyads.PLCTYPE_UINT,
|
||||
}
|
||||
|
||||
AdsHub.PLCTYPE_BOOL = pyads.PLCTYPE_BOOL
|
||||
AdsHub.PLCTYPE_BYTE = pyads.PLCTYPE_BYTE
|
||||
AdsHub.PLCTYPE_INT = pyads.PLCTYPE_INT
|
||||
AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT
|
||||
AdsHub.ADSError = pyads.ADSError
|
||||
|
||||
# connect to ads client and try to connect
|
||||
try:
|
||||
ads = AdsHub(client)
|
||||
except pyads.pyads.ADSError:
|
||||
_LOGGER.error(
|
||||
'Could not connect to ADS host (netid=%s, port=%s)', net_id, port
|
||||
)
|
||||
return False
|
||||
|
||||
# add ads hub to hass data collection, listen to shutdown
|
||||
hass.data[DATA_ADS] = ads
|
||||
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, ads.shutdown)
|
||||
|
||||
def handle_write_data_by_name(call):
|
||||
"""Write a value to the connected ADS device."""
|
||||
ads_var = call.data.get(CONF_ADS_VAR)
|
||||
ads_type = call.data.get(CONF_ADS_TYPE)
|
||||
value = call.data.get(CONF_ADS_VALUE)
|
||||
|
||||
try:
|
||||
ads.write_by_name(ads_var, value, ads.ADS_TYPEMAP[ads_type])
|
||||
except pyads.ADSError as err:
|
||||
_LOGGER.error(err)
|
||||
|
||||
# load descriptions from services.yaml
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
|
||||
descriptions[SERVICE_WRITE_DATA_BY_NAME],
|
||||
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# tuple to hold data needed for notification
|
||||
NotificationItem = namedtuple(
|
||||
'NotificationItem', 'hnotify huser name plc_datatype callback'
|
||||
)
|
||||
|
||||
|
||||
class AdsHub:
|
||||
"""Representation of a PyADS connection."""
|
||||
|
||||
def __init__(self, ads_client):
|
||||
"""Initialize the ADS Hub."""
|
||||
self._client = ads_client
|
||||
self._client.open()
|
||||
|
||||
# all ADS devices are registered here
|
||||
self._devices = []
|
||||
self._notification_items = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def shutdown(self, *args, **kwargs):
|
||||
"""Shutdown ADS connection."""
|
||||
_LOGGER.debug('Shutting down ADS')
|
||||
for notification_item in self._notification_items.values():
|
||||
self._client.del_device_notification(
|
||||
notification_item.hnotify,
|
||||
notification_item.huser
|
||||
)
|
||||
_LOGGER.debug(
|
||||
'Deleting device notification %d, %d',
|
||||
notification_item.hnotify, notification_item.huser
|
||||
)
|
||||
self._client.close()
|
||||
|
||||
def register_device(self, device):
|
||||
"""Register a new device."""
|
||||
self._devices.append(device)
|
||||
|
||||
def write_by_name(self, name, value, plc_datatype):
|
||||
"""Write a value to the device."""
|
||||
with self._lock:
|
||||
return self._client.write_by_name(name, value, plc_datatype)
|
||||
|
||||
def read_by_name(self, name, plc_datatype):
|
||||
"""Read a value from the device."""
|
||||
with self._lock:
|
||||
return self._client.read_by_name(name, plc_datatype)
|
||||
|
||||
def add_device_notification(self, name, plc_datatype, callback):
|
||||
"""Add a notification to the ADS devices."""
|
||||
from pyads import NotificationAttrib
|
||||
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
|
||||
|
||||
with self._lock:
|
||||
hnotify, huser = self._client.add_device_notification(
|
||||
name, attr, self._device_notification_callback
|
||||
)
|
||||
hnotify = int(hnotify)
|
||||
|
||||
_LOGGER.debug(
|
||||
'Added Device Notification %d for variable %s', hnotify, name
|
||||
)
|
||||
|
||||
self._notification_items[hnotify] = NotificationItem(
|
||||
hnotify, huser, name, plc_datatype, callback
|
||||
)
|
||||
|
||||
def _device_notification_callback(self, addr, notification, huser):
|
||||
"""Handle device notifications."""
|
||||
contents = notification.contents
|
||||
|
||||
hnotify = int(contents.hNotification)
|
||||
_LOGGER.debug('Received Notification %d', hnotify)
|
||||
data = contents.data
|
||||
|
||||
try:
|
||||
notification_item = self._notification_items[hnotify]
|
||||
except KeyError:
|
||||
_LOGGER.debug('Unknown Device Notification handle: %d', hnotify)
|
||||
return
|
||||
|
||||
# parse data to desired datatype
|
||||
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
|
||||
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_INT:
|
||||
value = struct.unpack('<h', bytearray(data)[:2])[0]
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
|
||||
value = struct.unpack('<B', bytearray(data)[:1])[0]
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
|
||||
value = struct.unpack('<H', bytearray(data)[:2])[0]
|
||||
else:
|
||||
value = bytearray(data)
|
||||
_LOGGER.warning('No callback available for this datatype.')
|
||||
|
||||
# execute callback
|
||||
notification_item.callback(notification_item.name, value)
|
||||
15
homeassistant/components/ads/services.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Describes the format for available ADS services
|
||||
|
||||
write_data_by_name:
|
||||
description: Write a value to the connected ADS device.
|
||||
|
||||
fields:
|
||||
adsvar:
|
||||
description: The name of the variable to write to.
|
||||
example: '.global_var'
|
||||
adstype:
|
||||
description: The data type of the variable to write to.
|
||||
example: 'int'
|
||||
value:
|
||||
description: The value to write to the variable.
|
||||
example: 1
|
||||
@@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_NIGHT)
|
||||
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
@@ -33,6 +33,7 @@ SERVICE_TO_METHOD = {
|
||||
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
|
||||
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
|
||||
SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night',
|
||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS: 'alarm_arm_custom_bypass',
|
||||
SERVICE_ALARM_TRIGGER: 'alarm_trigger'
|
||||
}
|
||||
|
||||
@@ -107,6 +108,18 @@ def alarm_trigger(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm custom bypass."""
|
||||
data = {}
|
||||
if code:
|
||||
data[ATTR_CODE] = code
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
@@ -216,6 +229,17 @@ class AlarmControlPanel(Entity):
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_trigger, code)
|
||||
|
||||
def alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_custom_bypass, code)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
||||
@@ -7,30 +7,21 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.components.alarmdecoder import (DATA_AD,
|
||||
SIGNAL_PANEL_MESSAGE)
|
||||
|
||||
from homeassistant.components.alarmdecoder import (
|
||||
DATA_AD, SIGNAL_PANEL_MESSAGE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
STATE_ALARM_TRIGGERED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up for AlarmDecoder alarm panels."""
|
||||
_LOGGER.debug("AlarmDecoderAlarmPanel: setup")
|
||||
|
||||
device = AlarmDecoderAlarmPanel("Alarm Panel", hass)
|
||||
|
||||
async_add_devices([device])
|
||||
add_devices([AlarmDecoderAlarmPanel()])
|
||||
|
||||
return True
|
||||
|
||||
@@ -38,38 +29,35 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
def __init__(self, name, hass):
|
||||
def __init__(self):
|
||||
"""Initialize the alarm panel."""
|
||||
self._display = ""
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
_LOGGER.debug("Setting up panel")
|
||||
self._name = "Alarm Panel"
|
||||
self._state = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
|
||||
@callback
|
||||
def _message_callback(self, message):
|
||||
if message.alarm_sounding or message.fire_alarm:
|
||||
if self._state != STATE_ALARM_TRIGGERED:
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
elif message.armed_away:
|
||||
if self._state != STATE_ALARM_ARMED_AWAY:
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
elif message.armed_home:
|
||||
if self._state != STATE_ALARM_ARMED_HOME:
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
else:
|
||||
if self._state != STATE_ALARM_DISARMED:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -91,26 +79,20 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
_LOGGER.debug("alarm_disarm: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("alarm_disarm: sending %s1", str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}1".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
_LOGGER.debug("alarm_arm_away: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("alarm_arm_away: sending %s2", str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}2".format(code))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
_LOGGER.debug("alarm_arm_home: %s", code)
|
||||
if code:
|
||||
_LOGGER.debug("alarm_arm_home: sending %s3", str(code))
|
||||
self.hass.data[DATA_AD].send("{!s}3".format(code))
|
||||
|
||||
@@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ARMED = 'armed'
|
||||
|
||||
CONF_HOME_MODE_NAME = 'home_mode_name'
|
||||
CONF_AWAY_MODE_NAME = 'away_mode_name'
|
||||
|
||||
DEPENDENCIES = ['arlo']
|
||||
|
||||
@@ -31,6 +32,7 @@ ICON = 'mdi:security'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
|
||||
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -43,19 +45,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
home_mode_name = config.get(CONF_HOME_MODE_NAME)
|
||||
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
|
||||
base_stations = []
|
||||
for base_station in data.base_stations:
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name))
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name,
|
||||
away_mode_name))
|
||||
async_add_devices(base_stations, True)
|
||||
|
||||
|
||||
class ArloBaseStation(AlarmControlPanel):
|
||||
"""Representation of an Arlo Alarm Control Panel."""
|
||||
|
||||
def __init__(self, data, home_mode_name):
|
||||
def __init__(self, data, home_mode_name, away_mode_name):
|
||||
"""Initialize the alarm control panel."""
|
||||
self._base_station = data
|
||||
self._home_mode_name = home_mode_name
|
||||
self._away_mode_name = away_mode_name
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
@@ -89,8 +94,8 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._base_station.mode = ARMED
|
||||
"""Send arm away command. Uses custom mode."""
|
||||
self._base_station.mode = self._away_mode_name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
@@ -118,4 +123,6 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
return STATE_ALARM_DISARMED
|
||||
elif mode == self._home_mode_name:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode == self._away_mode_name:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return None
|
||||
|
||||
92
homeassistant/components/alarm_control_panel/canary.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Support for Canary alarm.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.canary/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.canary import DATA_CANARY
|
||||
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
|
||||
|
||||
DEPENDENCIES = ['canary']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Canary alarms."""
|
||||
data = hass.data[DATA_CANARY]
|
||||
devices = []
|
||||
|
||||
for location in data.locations:
|
||||
devices.append(CanaryAlarm(data, location.location_id))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class CanaryAlarm(AlarmControlPanel):
|
||||
"""Representation of a Canary alarm control panel."""
|
||||
|
||||
def __init__(self, data, location_id):
|
||||
"""Initialize a Canary security camera."""
|
||||
self._data = data
|
||||
self._location_id = location_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alarm."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
return location.name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
|
||||
LOCATION_MODE_NIGHT
|
||||
|
||||
location = self._data.get_location(self._location_id)
|
||||
|
||||
if location.is_private:
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
mode = location.mode
|
||||
if mode.name == LOCATION_MODE_AWAY:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif mode.name == LOCATION_MODE_HOME:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode.name == LOCATION_MODE_NIGHT:
|
||||
return STATE_ALARM_ARMED_NIGHT
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
return {
|
||||
'private': location.is_private
|
||||
}
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
self._data.set_location_mode(self._location_id, location.mode.name,
|
||||
True)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from canary.api import LOCATION_MODE_HOME
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
from canary.api import LOCATION_MODE_AWAY
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
from canary.api import LOCATION_MODE_NIGHT
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
|
||||
@@ -4,27 +4,45 @@ Demo platform that has two fake alarm control panels.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import datetime
|
||||
import homeassistant.components.alarm_control_panel.manual as manual
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_TRIGGERED, CONF_PENDING_TIME)
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME,
|
||||
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo alarm control panel platform."""
|
||||
add_devices([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False, {
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
|
||||
STATE_ALARM_ARMED_AWAY: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_HOME: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_NIGHT: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_DISARMED: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_TRIGGERED: {
|
||||
CONF_PENDING_TIME: 5
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -116,12 +116,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the state of the device."""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll if no report server is enabled."""
|
||||
if not self._rs_enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
def handle_system_status_event(self, event):
|
||||
"""Handle egardia_system_status_event."""
|
||||
if event.data.get('status') is not None:
|
||||
statuscode = event.data.get('status')
|
||||
status = self.lookupstatusfromcode(statuscode)
|
||||
self.parsestatus(status)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def listen_to_system_status(self):
|
||||
"""Subscribe to egardia_system_status event."""
|
||||
@@ -161,9 +169,8 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def update(self):
|
||||
"""Update the alarm status."""
|
||||
if not self._rs_enabled:
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
107
homeassistant/components/alarm_control_panel/ialarm.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Interfaces with iAlarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.ialarm/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['pyialarm==0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'iAlarm'
|
||||
|
||||
|
||||
def no_application_protocol(value):
|
||||
"""Validate that value is without the application protocol."""
|
||||
protocol_separator = "://"
|
||||
if not value or protocol_separator in value:
|
||||
raise vol.Invalid(
|
||||
'Invalid host, {} is not allowed'.format(protocol_separator))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up an iAlarm control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
url = 'http://{}'.format(host)
|
||||
ialarm = IAlarmPanel(name, username, password, url)
|
||||
add_devices([ialarm], True)
|
||||
|
||||
|
||||
class IAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Represent an iAlarm status."""
|
||||
|
||||
def __init__(self, name, username, password, url):
|
||||
"""Initialize the iAlarm status."""
|
||||
from pyialarm import IAlarm
|
||||
|
||||
self._name = name
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._url = url
|
||||
self._state = None
|
||||
self._client = IAlarm(username, password, url)
|
||||
|
||||
@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
|
||||
|
||||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
status = self._client.get_status()
|
||||
_LOGGER.debug('iAlarm status: %s', status)
|
||||
if status:
|
||||
status = int(status)
|
||||
|
||||
if status == self._client.DISARMED:
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status == self._client.ARMED_AWAY:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
state = None
|
||||
|
||||
self._state = state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._client.disarm()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._client.arm_away()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._client.arm_stay()
|
||||
@@ -14,25 +14,42 @@ import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_DISARMED, STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, CONF_CODE,
|
||||
CONF_DELAY_TIME, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
|
||||
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED]
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PRETRIGGER_STATES:
|
||||
if CONF_DELAY_TIME not in config[state]:
|
||||
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
|
||||
if CONF_TRIGGER_TIME not in config[state]:
|
||||
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
@@ -40,26 +57,44 @@ def _state_validator(config):
|
||||
return config
|
||||
|
||||
|
||||
STATE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
def _state_schema(state):
|
||||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All({
|
||||
vol.Required(CONF_PLATFORM): 'manual',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
}, _state_validator))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -71,8 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
|
||||
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
|
||||
config.get(CONF_CODE_TEMPLATE),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config
|
||||
)])
|
||||
@@ -83,27 +117,37 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for 'trigger_time'. After that will be
|
||||
triggered for 'trigger_time', after that we return to the previous state
|
||||
or disarm if `disarm_after_trigger` is true.
|
||||
When triggered, will be pending for the triggering state's 'delay_time'
|
||||
plus the triggered state's 'pending_time'.
|
||||
After that will be triggered for 'trigger_time', after that we return to
|
||||
the previous state or disarm if `disarm_after_trigger` is true.
|
||||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time, trigger_time,
|
||||
def __init__(self, hass, name, code, code_template,
|
||||
disarm_after_trigger, config):
|
||||
"""Init the manual alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
if code_template:
|
||||
self._code = code_template
|
||||
self._code.hass = hass
|
||||
else:
|
||||
self._code = code or None
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._pre_trigger_state = self._state
|
||||
self._previous_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._pending_time_by_state = {}
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
self._pending_time_by_state[state] = datetime.timedelta(
|
||||
seconds=config[state][CONF_PENDING_TIME])
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -118,15 +162,16 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state == STATE_ALARM_TRIGGERED:
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time_by_state[self._state] +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
self._state = self._pre_trigger_state
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
@@ -135,9 +180,21 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
return self._state
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
@property
|
||||
def _active_state(self):
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
else:
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
return self._state_ts + pending_time > dt_util.utcnow()
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
pending_time += self._delay_time_by_state[self._previous_state]
|
||||
return pending_time
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
@@ -174,27 +231,43 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
def alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS):
|
||||
return
|
||||
|
||||
self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""
|
||||
Send alarm trigger command.
|
||||
|
||||
No code needed, a trigger time of zero for the current state
|
||||
disables the alarm.
|
||||
"""
|
||||
if not self._trigger_time_by_state[self._active_state]:
|
||||
return
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
if self._state == state:
|
||||
return
|
||||
|
||||
self._previous_state = self._state
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
|
||||
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._trigger_time + pending_time)
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
@@ -202,7 +275,14 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if self._code is None:
|
||||
return True
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
@@ -213,6 +293,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
@@ -16,8 +16,8 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER)
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
||||
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
@@ -26,28 +26,44 @@ from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
|
||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
||||
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
||||
DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
|
||||
DEFAULT_DISARM = 'DISARM'
|
||||
|
||||
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED]
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
config = copy.deepcopy(config)
|
||||
for state in SUPPORTED_PRETRIGGER_STATES:
|
||||
if CONF_DELAY_TIME not in config[state]:
|
||||
config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME]
|
||||
if CONF_TRIGGER_TIME not in config[state]:
|
||||
config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
if CONF_PENDING_TIME not in config[state]:
|
||||
config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME]
|
||||
@@ -55,27 +71,44 @@ def _state_validator(config):
|
||||
return config
|
||||
|
||||
|
||||
STATE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||
})
|
||||
def _state_schema(state):
|
||||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PLATFORM): 'manual_mqtt',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
@@ -93,8 +126,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
|
||||
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
|
||||
config.get(CONF_CODE_TEMPLATE),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config.get(mqtt.CONF_STATE_TOPIC),
|
||||
config.get(mqtt.CONF_COMMAND_TOPIC),
|
||||
@@ -111,13 +143,15 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for 'trigger_time'. After that will be
|
||||
triggered for 'trigger_time', after that we return to the previous state
|
||||
or disarm if `disarm_after_trigger` is true.
|
||||
When triggered, will be pending for the triggering state's 'delay_time'
|
||||
plus the triggered state's 'pending_time'.
|
||||
After that will be triggered for 'trigger_time', after that we return to
|
||||
the previous state or disarm if `disarm_after_trigger` is true.
|
||||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time,
|
||||
trigger_time, disarm_after_trigger,
|
||||
def __init__(self, hass, name, code, code_template,
|
||||
disarm_after_trigger,
|
||||
state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away,
|
||||
payload_arm_night, config):
|
||||
@@ -125,17 +159,24 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._pending_time = datetime.timedelta(seconds=pending_time)
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
if code_template:
|
||||
self._code = code_template
|
||||
self._code.hass = hass
|
||||
else:
|
||||
self._code = code or None
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._pre_trigger_state = self._state
|
||||
self._previous_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._pending_time_by_state = {}
|
||||
for state in SUPPORTED_PENDING_STATES:
|
||||
self._pending_time_by_state[state] = datetime.timedelta(
|
||||
seconds=config[state][CONF_PENDING_TIME])
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
@@ -158,15 +199,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state == STATE_ALARM_TRIGGERED:
|
||||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time_by_state[self._state] +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
self._state = self._pre_trigger_state
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
@@ -175,9 +217,21 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
return self._state
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
@property
|
||||
def _active_state(self):
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
return self._previous_state
|
||||
else:
|
||||
return self._state
|
||||
|
||||
def _pending_time(self, state):
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
return self._state_ts + pending_time > dt_util.utcnow()
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
pending_time += self._delay_time_by_state[self._previous_state]
|
||||
return pending_time
|
||||
|
||||
def _within_pending_time(self, state):
|
||||
return self._state_ts + self._pending_time(state) > dt_util.utcnow()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
@@ -215,26 +269,35 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
self._update_state(STATE_ALARM_ARMED_NIGHT)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
"""
|
||||
Send alarm trigger command.
|
||||
|
||||
No code needed, a trigger time of zero for the current state
|
||||
disables the alarm.
|
||||
"""
|
||||
if not self._trigger_time_by_state[self._active_state]:
|
||||
return
|
||||
self._update_state(STATE_ALARM_TRIGGERED)
|
||||
|
||||
def _update_state(self, state):
|
||||
if self._state == state:
|
||||
return
|
||||
|
||||
self._previous_state = self._state
|
||||
self._state = state
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
pending_time = self._pending_time_by_state[state]
|
||||
|
||||
if state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._trigger_time + pending_time)
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
@@ -242,7 +305,14 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if self._code is None:
|
||||
return True
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
@@ -253,6 +323,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
||||
@@ -14,9 +14,11 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.13']
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.16']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -76,6 +78,8 @@ class TotalConnect(alarm.AlarmControlPanel):
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif status == self._client.ARMED_STAY_NIGHT:
|
||||
state = STATE_ALARM_ARMED_NIGHT
|
||||
elif status == self._client.ARMED_CUSTOM_BYPASS:
|
||||
state = STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||
elif status == self._client.ARMING:
|
||||
state = STATE_ALARM_ARMING
|
||||
elif status == self._client.DISARMING:
|
||||
|
||||
@@ -4,16 +4,13 @@ Support for AlarmDecoder devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarmdecoder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['alarmdecoder==0.12.3']
|
||||
|
||||
@@ -71,9 +68,9 @@ ZONE_SCHEMA = vol.Schema({
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA,
|
||||
DEVICE_SERIAL_SCHEMA,
|
||||
DEVICE_USB_SCHEMA),
|
||||
vol.Required(CONF_DEVICE): vol.Any(
|
||||
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA,
|
||||
DEVICE_USB_SCHEMA),
|
||||
vol.Optional(CONF_PANEL_DISPLAY,
|
||||
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
@@ -81,8 +78,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
def setup(hass, config):
|
||||
"""Set up for the AlarmDecoder devices."""
|
||||
from alarmdecoder import AlarmDecoder
|
||||
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
|
||||
@@ -99,32 +95,25 @@ def async_setup(hass, config):
|
||||
path = DEFAULT_DEVICE_PATH
|
||||
baud = DEFAULT_DEVICE_BAUD
|
||||
|
||||
sync_connect = asyncio.Future(loop=hass.loop)
|
||||
|
||||
def handle_open(device):
|
||||
"""Handle the successful connection."""
|
||||
_LOGGER.info("Established a connection with the alarmdecoder")
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
||||
sync_connect.set_result(True)
|
||||
|
||||
@callback
|
||||
def stop_alarmdecoder(event):
|
||||
"""Handle the shutdown of AlarmDecoder."""
|
||||
_LOGGER.debug("Shutting down alarmdecoder")
|
||||
controller.close()
|
||||
|
||||
@callback
|
||||
def handle_message(sender, message):
|
||||
"""Handle message from AlarmDecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message)
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_PANEL_MESSAGE, message)
|
||||
|
||||
def zone_fault_callback(sender, zone):
|
||||
"""Handle zone fault from AlarmDecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone)
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_FAULT, zone)
|
||||
|
||||
def zone_restore_callback(sender, zone):
|
||||
"""Handle zone restore from AlarmDecoder."""
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone)
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_RESTORE, zone)
|
||||
|
||||
controller = False
|
||||
if device_type == 'socket':
|
||||
@@ -139,7 +128,6 @@ def async_setup(hass, config):
|
||||
AlarmDecoder(USBDevice.find())
|
||||
return False
|
||||
|
||||
controller.on_open += handle_open
|
||||
controller.on_message += handle_message
|
||||
controller.on_zone_fault += zone_fault_callback
|
||||
controller.on_zone_restore += zone_restore_callback
|
||||
@@ -148,21 +136,16 @@ def async_setup(hass, config):
|
||||
|
||||
controller.open(baud)
|
||||
|
||||
result = yield from sync_connect
|
||||
_LOGGER.debug("Established a connection with the alarmdecoder")
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
||||
|
||||
if not result:
|
||||
return False
|
||||
|
||||
hass.async_add_job(
|
||||
async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf,
|
||||
config))
|
||||
load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
|
||||
|
||||
if zones:
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config))
|
||||
load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)
|
||||
|
||||
if display:
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'sensor', DOMAIN, conf, config))
|
||||
load_platform(hass, 'sensor', DOMAIN, conf, config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -171,7 +171,7 @@ def async_api_discovery(hass, config, request):
|
||||
|
||||
# Required description as per Amazon Scene docs
|
||||
if entity.domain == scene.DOMAIN:
|
||||
scene_fmt = '%s (Scene connected via Home Assistant)'
|
||||
scene_fmt = '{} (Scene connected via Home Assistant)'
|
||||
description = scene_fmt.format(description)
|
||||
|
||||
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES
|
||||
@@ -245,7 +245,7 @@ def async_api_turn_on(hass, config, request, entity):
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -261,7 +261,7 @@ def async_api_turn_off(hass, config, request, entity):
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -276,7 +276,7 @@ def async_api_set_brightness(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -300,7 +300,7 @@ def async_api_adjust_brightness(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -321,14 +321,14 @@ def async_api_set_color(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=True)
|
||||
}, 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=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -343,7 +343,7 @@ def async_api_set_color_temperature(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_KELVIN: kelvin,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -361,7 +361,7 @@ def async_api_decrease_color_temp(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -379,7 +379,7 @@ def async_api_increase_color_temp(hass, config, request, entity):
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -391,7 +391,7 @@ def async_api_activate(hass, config, request, entity):
|
||||
"""Process a activate request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -421,8 +421,8 @@ def async_api_set_percentage(hass, config, request, entity):
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = percentage
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -469,8 +469,8 @@ def async_api_adjust_percentage(hass, config, request, entity):
|
||||
|
||||
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -482,7 +482,7 @@ def async_api_lock(hass, config, request, entity):
|
||||
"""Process a lock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -495,7 +495,7 @@ def async_api_unlock(hass, config, request, entity):
|
||||
"""Process a unlock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -512,8 +512,9 @@ def async_api_set_volume(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -540,9 +541,9 @@ def async_api_adjust_volume(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -559,9 +560,9 @@ def async_api_set_mute(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_MUTE,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_MUTE,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -575,8 +576,9 @@ def async_api_play(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -590,8 +592,9 @@ def async_api_pause(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -605,8 +608,9 @@ def async_api_stop(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -620,9 +624,9 @@ def async_api_next(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -636,8 +640,8 @@ def async_api_previous(hass, config, request, entity):
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, blocking=True)
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -89,6 +89,7 @@ def setup(hass, config):
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera
|
||||
|
||||
hass.data[DATA_AMCREST] = {}
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
@@ -126,22 +127,34 @@ def setup(hass, config):
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
||||
camera, name, authentication, ffmpeg_arguments, stream_source,
|
||||
resolution)
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_AUTHENTICATION: authentication,
|
||||
CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments,
|
||||
CONF_NAME: name,
|
||||
CONF_RESOLUTION: resolution,
|
||||
CONF_STREAM_SOURCE: stream_source,
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestDevice(object):
|
||||
"""Representation of a base Amcrest discovery device."""
|
||||
|
||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
||||
stream_source, resolution):
|
||||
"""Initialize the entity."""
|
||||
self.device = camera
|
||||
self.name = name
|
||||
self.authentication = authentication
|
||||
self.ffmpeg_arguments = ffmpeg_arguments
|
||||
self.stream_source = stream_source
|
||||
self.resolution = resolution
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.components.discovery import SERVICE_APPLE_TV
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.8']
|
||||
REQUIREMENTS = ['pyatv==0.3.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ def async_trigger(hass, config, action):
|
||||
above = config.get(CONF_ABOVE)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
async_remove_track_same = None
|
||||
already_triggered = False
|
||||
unsub_track_same = {}
|
||||
entities_triggered = set()
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
@@ -63,8 +63,6 @@ def async_trigger(hass, config, action):
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal already_triggered, async_remove_track_same
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
@@ -81,16 +79,18 @@ def async_trigger(hass, config, action):
|
||||
|
||||
matching = check_numeric_state(entity, from_s, to_s)
|
||||
|
||||
if matching and not already_triggered:
|
||||
if not matching:
|
||||
entities_triggered.discard(entity)
|
||||
elif entity not in entities_triggered:
|
||||
entities_triggered.add(entity)
|
||||
|
||||
if time_delta:
|
||||
async_remove_track_same = async_track_same_state(
|
||||
unsub_track_same[entity] = async_track_same_state(
|
||||
hass, time_delta, call_action, entity_ids=entity_id,
|
||||
async_check_same_func=check_numeric_state)
|
||||
else:
|
||||
call_action()
|
||||
|
||||
already_triggered = matching
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
|
||||
@@ -98,7 +98,8 @@ def async_trigger(hass, config, action):
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
if async_remove_track_same:
|
||||
async_remove_track_same() # pylint: disable=not-callable
|
||||
for async_remove in unsub_track_same.values():
|
||||
async_remove()
|
||||
unsub_track_same.clear()
|
||||
|
||||
return async_remove
|
||||
|
||||
@@ -35,13 +35,11 @@ def async_trigger(hass, config, action):
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
||||
async_remove_track_same = None
|
||||
unsub_track_same = {}
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal async_remove_track_same
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
@@ -64,7 +62,7 @@ def async_trigger(hass, config, action):
|
||||
call_action()
|
||||
return
|
||||
|
||||
async_remove_track_same = async_track_same_state(
|
||||
unsub_track_same[entity] = async_track_same_state(
|
||||
hass, time_delta, call_action,
|
||||
lambda _, _2, to_state: to_state.state == to_s.state,
|
||||
entity_ids=entity_id)
|
||||
@@ -76,7 +74,8 @@ def async_trigger(hass, config, action):
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
if async_remove_track_same:
|
||||
async_remove_track_same() # pylint: disable=not-callable
|
||||
for async_remove in unsub_track_same.values():
|
||||
async_remove()
|
||||
unsub_track_same.clear()
|
||||
|
||||
return async_remove
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/axis/
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -22,6 +21,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
|
||||
REQUIREMENTS = ['axis==14']
|
||||
@@ -103,9 +103,9 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||
return False
|
||||
|
||||
if setup_device(hass, config, device_config):
|
||||
config_file = _read_config(hass)
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
config_file[serialnumber] = dict(device_config)
|
||||
_write_config(hass, config_file)
|
||||
save_json(hass.config.path(CONFIG_FILE), config_file)
|
||||
configurator.request_done(request_id)
|
||||
else:
|
||||
configurator.notify_errors(request_id,
|
||||
@@ -163,7 +163,7 @@ def setup(hass, config):
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
|
||||
if serialnumber not in AXIS_DEVICES:
|
||||
config_file = _read_config(hass)
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
if serialnumber in config_file:
|
||||
# Device config previously saved to file
|
||||
try:
|
||||
@@ -274,25 +274,6 @@ def setup_device(hass, config, device_config):
|
||||
return True
|
||||
|
||||
|
||||
def _read_config(hass):
|
||||
"""Read Axis config."""
|
||||
path = hass.config.path(CONFIG_FILE)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return {}
|
||||
|
||||
with open(path) as f_handle:
|
||||
# Guard against empty file
|
||||
return json.loads(f_handle.read() or '{}')
|
||||
|
||||
|
||||
def _write_config(hass, config):
|
||||
"""Write Axis config."""
|
||||
data = json.dumps(config)
|
||||
with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(data)
|
||||
|
||||
|
||||
class AxisDeviceEvent(Entity):
|
||||
"""Representation of a Axis device event."""
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
DEVICE_CLASSES = [
|
||||
'battery', # On means low, Off means normal
|
||||
'cold', # On means cold (or too cold)
|
||||
'connectivity', # On means connection present, Off = no connection
|
||||
'gas', # CO, CO2, etc.
|
||||
@@ -32,6 +33,8 @@ DEVICE_CLASSES = [
|
||||
'opening', # Door, window, etc.
|
||||
'plug', # On means plugged in, Off means unplugged
|
||||
'power', # Power, over-current, etc
|
||||
'presence', # On means home, Off means away
|
||||
'problem', # On means there is a problem, Off means the status is OK
|
||||
'safety', # Generic on=unsafe, off=safe
|
||||
'smoke', # Smoke detector
|
||||
'sound', # On means sound detected, Off means no sound
|
||||
|
||||
87
homeassistant/components/binary_sensor/ads.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Support for ADS binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation.
|
||||
https://home-assistant.io/components/binary_sensor.ads/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, \
|
||||
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA
|
||||
from homeassistant.components.ads import DATA_ADS, CONF_ADS_VAR
|
||||
from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['ads']
|
||||
DEFAULT_NAME = 'ADS binary sensor'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Binary Sensor platform for ADS."""
|
||||
ads_hub = hass.data.get(DATA_ADS)
|
||||
|
||||
ads_var = config.get(CONF_ADS_VAR)
|
||||
name = config.get(CONF_NAME)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
|
||||
add_devices([ads_sensor])
|
||||
|
||||
|
||||
class AdsBinarySensor(BinarySensorDevice):
|
||||
"""Representation of ADS binary sensors."""
|
||||
|
||||
def __init__(self, ads_hub, name, ads_var, device_class):
|
||||
"""Initialize AdsBinarySensor entity."""
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._device_class = device_class or 'moving'
|
||||
self._ads_hub = ads_hub
|
||||
self.ads_var = ads_var
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
def update(name, value):
|
||||
"""Handle device notifications."""
|
||||
_LOGGER.debug('Variable %s changed its value to %d',
|
||||
name, value)
|
||||
self._state = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.hass.async_add_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the default name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False because entity pushes its state to HA."""
|
||||
return False
|
||||
@@ -7,39 +7,29 @@ https://home-assistant.io/components/binary_sensor.alarmdecoder/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
from homeassistant.components.alarmdecoder import (ZONE_SCHEMA,
|
||||
CONF_ZONES,
|
||||
CONF_ZONE_NAME,
|
||||
CONF_ZONE_TYPE,
|
||||
SIGNAL_ZONE_FAULT,
|
||||
SIGNAL_ZONE_RESTORE)
|
||||
|
||||
from homeassistant.components.alarmdecoder import (
|
||||
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
||||
SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the AlarmDecoder binary sensor devices."""
|
||||
configured_zones = discovery_info[CONF_ZONES]
|
||||
|
||||
devices = []
|
||||
|
||||
for zone_num in configured_zones:
|
||||
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
device = AlarmDecoderBinarySensor(
|
||||
hass, zone_num, zone_name, zone_type)
|
||||
device = AlarmDecoderBinarySensor(zone_num, zone_name, zone_type)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
add_devices(devices)
|
||||
|
||||
return True
|
||||
|
||||
@@ -47,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an AlarmDecoder binary sensor."""
|
||||
|
||||
def __init__(self, hass, zone_number, zone_name, zone_type):
|
||||
def __init__(self, zone_number, zone_name, zone_type):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._zone_type = zone_type
|
||||
@@ -55,16 +45,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
self._name = zone_name
|
||||
self._type = zone_type
|
||||
|
||||
_LOGGER.debug("Setup up zone: %s", self._name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback)
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_ZONE_RESTORE, self._restore_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -97,16 +85,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@callback
|
||||
def _fault_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 1
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _restore_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self._state = 0
|
||||
self.async_schedule_update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -62,6 +62,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return False
|
||||
|
||||
# The order of zones returned by client.list_zones() can vary.
|
||||
# When the zones are not named, this can result in the same entity
|
||||
# name mapping to different sensors in an unpredictable way. Sort
|
||||
# the zones by zone number to prevent this.
|
||||
|
||||
client.zones.sort(key=lambda zone: zone['number'])
|
||||
|
||||
for zone in client.zones:
|
||||
_LOGGER.info("Loading Zone found: %s", zone['name'])
|
||||
if zone['number'] not in exclude:
|
||||
@@ -118,7 +125,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
# True means "faulted" or "open" or "abnormal state"
|
||||
return bool(self._zone['state'] == 'Normal')
|
||||
return bool(self._zone['state'] != 'Normal')
|
||||
|
||||
def update(self):
|
||||
"""Get updated stats from API."""
|
||||
|
||||
63
homeassistant/components/binary_sensor/hive.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Support for the Hive devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.hive/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.hive import DATA_HIVE
|
||||
|
||||
DEPENDENCIES = ['hive']
|
||||
|
||||
DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion',
|
||||
'contactsensor': 'opening'}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Hive sensor devices."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
session = hass.data.get(DATA_HIVE)
|
||||
|
||||
add_devices([HiveBinarySensorEntity(session, discovery_info)])
|
||||
|
||||
|
||||
class HiveBinarySensorEntity(BinarySensorDevice):
|
||||
"""Representation of a Hive binary sensor."""
|
||||
|
||||
def __init__(self, hivesession, hivedevice):
|
||||
"""Initialize the hive sensor."""
|
||||
self.node_id = hivedevice["Hive_NodeID"]
|
||||
self.node_name = hivedevice["Hive_NodeName"]
|
||||
self.device_type = hivedevice["HA_DeviceType"]
|
||||
self.node_device_type = hivedevice["Hive_DeviceType"]
|
||||
self.session = hivesession
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
|
||||
self.session.entities.append(self)
|
||||
|
||||
def handle_update(self, updatesource):
|
||||
"""Handle the new update request."""
|
||||
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEVICETYPE_DEVICE_CLASS.get(self.node_device_type)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self.node_name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.session.sensor.get_state(self.node_id,
|
||||
self.node_device_type)
|
||||
|
||||
def update(self):
|
||||
"""Update all Node data frome Hive."""
|
||||
self.session.core.update_data(self.node_id)
|
||||
@@ -25,6 +25,7 @@ SENSOR_TYPES_CLASS = {
|
||||
'RemoteMotion': None,
|
||||
'WeatherSensor': None,
|
||||
'TiltSensor': None,
|
||||
'PresenceIP': 'motion',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,24 +4,31 @@ Support for ISY994 binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.isy994/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Callable # noqa
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
False: STATE_OFF,
|
||||
True: STATE_ON,
|
||||
}
|
||||
|
||||
UOM = ['2', '78']
|
||||
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
|
||||
|
||||
ISY_DEVICE_TYPES = {
|
||||
'moisture': ['16.8', '16.13', '16.14'],
|
||||
'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'],
|
||||
'motion': ['16.1', '16.4', '16.5', '16.3']
|
||||
}
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
@@ -32,10 +39,53 @@ def setup_platform(hass, config: ConfigType,
|
||||
return False
|
||||
|
||||
devices = []
|
||||
devices_by_nid = {}
|
||||
child_nodes = []
|
||||
|
||||
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
|
||||
states=STATES):
|
||||
devices.append(ISYBinarySensorDevice(node))
|
||||
if node.parent_node is None:
|
||||
device = ISYBinarySensorDevice(node)
|
||||
devices.append(device)
|
||||
devices_by_nid[node.nid] = device
|
||||
else:
|
||||
# We'll process the child nodes last, to ensure all parent nodes
|
||||
# have been processed
|
||||
child_nodes.append(node)
|
||||
|
||||
for node in child_nodes:
|
||||
try:
|
||||
parent_device = devices_by_nid[node.parent_node.nid]
|
||||
except KeyError:
|
||||
_LOGGER.error("Node %s has a parent node %s, but no device "
|
||||
"was created for the parent. Skipping.",
|
||||
node.nid, node.parent_nid)
|
||||
else:
|
||||
device_type = _detect_device_type(node)
|
||||
subnode_id = int(node.nid[-1])
|
||||
if device_type == 'opening':
|
||||
# Door/window sensors use an optional "negative" node
|
||||
if subnode_id == 4:
|
||||
# Subnode 4 is the heartbeat node, which we will represent
|
||||
# as a separate binary_sensor
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
elif subnode_id == 2:
|
||||
parent_device.add_negative_node(node)
|
||||
elif device_type == 'moisture':
|
||||
# Moisure nodes have a subnode 2, but we ignore it because it's
|
||||
# just the inverse of the primary node.
|
||||
if subnode_id == 4:
|
||||
# Heartbeat node
|
||||
device = ISYBinarySensorHeartbeat(node, parent_device)
|
||||
parent_device.add_heartbeat_device(device)
|
||||
devices.append(device)
|
||||
else:
|
||||
# We don't yet have any special logic for other sensor types,
|
||||
# so add the nodes as individual devices
|
||||
device = ISYBinarySensorDevice(node)
|
||||
devices.append(device)
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
@@ -48,23 +98,282 @@ def setup_platform(hass, config: ConfigType,
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
def _detect_device_type(node) -> str:
|
||||
try:
|
||||
device_type = node.type
|
||||
except AttributeError:
|
||||
# The type attribute didn't exist in the ISY's API response
|
||||
return None
|
||||
|
||||
split_type = device_type.split('.')
|
||||
for device_class, ids in ISY_DEVICE_TYPES.items():
|
||||
if '{}.{}'.format(split_type[0], split_type[1]) in ids:
|
||||
return device_class
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _is_val_unknown(val):
|
||||
"""Determine if a number value represents UNKNOWN from PyISY."""
|
||||
return val == -1*float('inf')
|
||||
|
||||
|
||||
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor device."""
|
||||
"""Representation of an ISY994 binary sensor device.
|
||||
|
||||
Often times, a single device is represented by multiple nodes in the ISY,
|
||||
allowing for different nuances in how those devices report their on and
|
||||
off events. This class turns those multiple nodes in to a single Hass
|
||||
entity and handles both ways that ISY binary sensors can work.
|
||||
"""
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
self._negative_node = None
|
||||
self._heartbeat_device = None
|
||||
self._device_class_from_type = _detect_device_type(self._node)
|
||||
# pylint: disable=protected-access
|
||||
if _is_val_unknown(self._node.status._val):
|
||||
self._computed_state = None
|
||||
else:
|
||||
self._computed_state = bool(self._node.status._val)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node and subnode event emitters."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
self._node.controlEvents.subscribe(self._positive_node_control_handler)
|
||||
|
||||
if self._negative_node is not None:
|
||||
self._negative_node.controlEvents.subscribe(
|
||||
self._negative_node_control_handler)
|
||||
|
||||
def add_heartbeat_device(self, device) -> None:
|
||||
"""Register a heartbeat device for this sensor.
|
||||
|
||||
The heartbeat node beats on its own, but we can gain a little
|
||||
reliability by considering any node activity for this sensor
|
||||
to be a heartbeat as well.
|
||||
"""
|
||||
self._heartbeat_device = device
|
||||
|
||||
def _heartbeat(self) -> None:
|
||||
"""Send a heartbeat to our heartbeat device, if we have one."""
|
||||
if self._heartbeat_device is not None:
|
||||
self._heartbeat_device.heartbeat()
|
||||
|
||||
def add_negative_node(self, child) -> None:
|
||||
"""Add a negative node to this binary sensor device.
|
||||
|
||||
The negative node is a node that can receive the 'off' events
|
||||
for the sensor, depending on device configuration and type.
|
||||
"""
|
||||
self._negative_node = child
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if not _is_val_unknown(self._negative_node.status._val):
|
||||
# If the negative node has a value, it means the negative node is
|
||||
# in use for this device. Therefore, we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
|
||||
def _negative_node_control_handler(self, event: object) -> None:
|
||||
"""Handle an "On" control event from the "negative" node."""
|
||||
if event == 'DON':
|
||||
_LOGGER.debug("Sensor %s turning Off via the Negative node "
|
||||
"sending a DON command", self.name)
|
||||
self._computed_state = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
def _positive_node_control_handler(self, event: object) -> None:
|
||||
"""Handle On and Off control event coming from the primary node.
|
||||
|
||||
Depending on device configuration, sometimes only On events
|
||||
will come to this node, with the negative node representing Off
|
||||
events
|
||||
"""
|
||||
if event == 'DON':
|
||||
_LOGGER.debug("Sensor %s turning On via the Primary node "
|
||||
"sending a DON command", self.name)
|
||||
self._computed_state = True
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
if event == 'DOF':
|
||||
_LOGGER.debug("Sensor %s turning Off via the Primary node "
|
||||
"sending a DOF command", self.name)
|
||||
self._computed_state = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore primary node status updates.
|
||||
|
||||
We listen directly to the Control events on all nodes for this
|
||||
device.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
"""Get the current value of the device.
|
||||
|
||||
Insteon leak sensors set their primary node to On when the state is
|
||||
DRY, not WET, so we invert the binary state if the user indicates
|
||||
that it is a moisture sensor.
|
||||
"""
|
||||
if self._computed_state is None:
|
||||
# Do this first so we don't invert None on moisture sensors
|
||||
return None
|
||||
|
||||
if self.device_class == 'moisture':
|
||||
return not self._computed_state
|
||||
|
||||
return self._computed_state
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 binary sensor device is on.
|
||||
|
||||
Note: This method will return false if the current state is UNKNOWN
|
||||
"""
|
||||
return bool(self.value)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
if self._computed_state is None:
|
||||
return None
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Return the class of this device.
|
||||
|
||||
This was discovered by parsing the device type code during init
|
||||
"""
|
||||
return self._device_class_from_type
|
||||
|
||||
|
||||
class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice):
|
||||
"""Representation of the battery state of an ISY994 sensor."""
|
||||
|
||||
def __init__(self, node, parent_device) -> None:
|
||||
"""Initialize the ISY994 binary sensor device."""
|
||||
super().__init__(node)
|
||||
self._computed_state = None
|
||||
self._parent_device = parent_device
|
||||
self._heartbeat_timer = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node and subnode event emitters."""
|
||||
yield from super().async_added_to_hass()
|
||||
|
||||
self._node.controlEvents.subscribe(
|
||||
self._heartbeat_node_control_handler)
|
||||
|
||||
# Start the timer on bootup, so we can change from UNKNOWN to ON
|
||||
self._restart_timer()
|
||||
|
||||
def _heartbeat_node_control_handler(self, event: object) -> None:
|
||||
"""Update the heartbeat timestamp when an On event is sent."""
|
||||
if event == 'DON':
|
||||
self.heartbeat()
|
||||
|
||||
def heartbeat(self):
|
||||
"""Mark the device as online, and restart the 25 hour timer.
|
||||
|
||||
This gets called when the heartbeat node beats, but also when the
|
||||
parent sensor sends any events, as we can trust that to mean the device
|
||||
is online. This mitigates the risk of false positives due to a single
|
||||
missed heartbeat event.
|
||||
"""
|
||||
self._computed_state = False
|
||||
self._restart_timer()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _restart_timer(self):
|
||||
"""Restart the 25 hour timer."""
|
||||
try:
|
||||
self._heartbeat_timer()
|
||||
self._heartbeat_timer = None
|
||||
except TypeError:
|
||||
# No heartbeat timer is active
|
||||
pass
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@callback
|
||||
def timer_elapsed(now) -> None:
|
||||
"""Heartbeat missed; set state to indicate dead battery."""
|
||||
self._computed_state = True
|
||||
self._heartbeat_timer = None
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
point_in_time = dt_util.utcnow() + timedelta(hours=25)
|
||||
_LOGGER.debug("Timer starting. Now: %s Then: %s",
|
||||
dt_util.utcnow(), point_in_time)
|
||||
|
||||
self._heartbeat_timer = async_track_point_in_utc_time(
|
||||
self.hass, timer_elapsed, point_in_time)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore node status updates.
|
||||
|
||||
We listen directly to the Control events for this device.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
"""Get the current value of this sensor."""
|
||||
return self._computed_state
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 binary sensor device is on.
|
||||
|
||||
Note: This method will return false if the current state is UNKNOWN
|
||||
"""
|
||||
return bool(self.value)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
if self._computed_state is None:
|
||||
return None
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Get the class of this device."""
|
||||
return 'battery'
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Get the state attributes for the device."""
|
||||
attr = super().device_state_attributes
|
||||
attr['parent_entity_id'] = self._parent_device.entity_id
|
||||
return attr
|
||||
|
||||
|
||||
class ISYBinarySensorProgram(isy.ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor program.
|
||||
|
||||
This does not need all of the subnode logic in the device version of binary
|
||||
sensors.
|
||||
"""
|
||||
|
||||
def __init__(self, name, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor program."""
|
||||
super().__init__(node)
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 binary sensor device is on."""
|
||||
return bool(self.value)
|
||||
|
||||
|
||||
class ISYBinarySensorProgram(ISYBinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor program."""
|
||||
|
||||
def __init__(self, name, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor program."""
|
||||
ISYBinarySensorDevice.__init__(self, node)
|
||||
self._name = name
|
||||
|
||||
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Vera controller devices."""
|
||||
add_devices(
|
||||
VeraBinarySensor(device, VERA_CONTROLLER)
|
||||
for device in VERA_DEVICES['binary_sensor'])
|
||||
VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
|
||||
for device in hass.data[VERA_DEVICES]['binary_sensor'])
|
||||
|
||||
|
||||
class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
|
||||
230
homeassistant/components/calendar/caldav.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
Support for WebDav Calendar.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar.caldav/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.calendar import (
|
||||
CalendarEventDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
|
||||
from homeassistant.util import dt, Throttle
|
||||
|
||||
REQUIREMENTS = ['caldav==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
CONF_CALENDARS = 'calendars'
|
||||
CONF_CUSTOM_CALENDARS = 'custom_calendars'
|
||||
CONF_CALENDAR = 'calendar'
|
||||
CONF_SEARCH = 'search'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Required(CONF_URL): vol.Url(),
|
||||
vol.Optional(CONF_CALENDARS, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Schema([
|
||||
cv.string
|
||||
])),
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_CUSTOM_CALENDARS, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Schema([
|
||||
vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_CALENDAR): cv.string,
|
||||
vol.Required(CONF_SEARCH): cv.string
|
||||
})
|
||||
]))
|
||||
})
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
"""Set up the WebDav Calendar platform."""
|
||||
import caldav
|
||||
|
||||
client = caldav.DAVClient(config.get(CONF_URL),
|
||||
None,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD))
|
||||
|
||||
# Retrieve all the remote calendars
|
||||
calendars = client.principal().calendars()
|
||||
|
||||
calendar_devices = []
|
||||
for calendar in list(calendars):
|
||||
# If a calendar name was given in the configuration,
|
||||
# ignore all the others
|
||||
if (config.get(CONF_CALENDARS)
|
||||
and calendar.name not in config.get(CONF_CALENDARS)):
|
||||
_LOGGER.debug("Ignoring calendar '%s'", calendar.name)
|
||||
continue
|
||||
|
||||
# Create additional calendars based on custom filtering
|
||||
# rules
|
||||
for cust_calendar in config.get(CONF_CUSTOM_CALENDARS):
|
||||
# Check that the base calendar matches
|
||||
if cust_calendar.get(CONF_CALENDAR) != calendar.name:
|
||||
continue
|
||||
|
||||
device_data = {
|
||||
CONF_NAME: cust_calendar.get(CONF_NAME),
|
||||
CONF_DEVICE_ID: "{} {}".format(
|
||||
cust_calendar.get(CONF_CALENDAR),
|
||||
cust_calendar.get(CONF_NAME)),
|
||||
}
|
||||
|
||||
calendar_devices.append(
|
||||
WebDavCalendarEventDevice(hass,
|
||||
device_data,
|
||||
calendar,
|
||||
True,
|
||||
cust_calendar.get(CONF_SEARCH))
|
||||
)
|
||||
|
||||
# Create a default calendar if there was no custom one
|
||||
if not config.get(CONF_CUSTOM_CALENDARS):
|
||||
device_data = {
|
||||
CONF_NAME: calendar.name,
|
||||
CONF_DEVICE_ID: calendar.name
|
||||
}
|
||||
calendar_devices.append(
|
||||
WebDavCalendarEventDevice(hass, device_data, calendar)
|
||||
)
|
||||
|
||||
# Finally add all the calendars we've created
|
||||
add_devices(calendar_devices)
|
||||
|
||||
|
||||
class WebDavCalendarEventDevice(CalendarEventDevice):
|
||||
"""A device for getting the next Task from a WebDav Calendar."""
|
||||
|
||||
def __init__(self,
|
||||
hass,
|
||||
device_data,
|
||||
calendar,
|
||||
all_day=False,
|
||||
search=None):
|
||||
"""Create the WebDav Calendar Event Device."""
|
||||
self.data = WebDavCalendarData(calendar, all_day, search)
|
||||
super().__init__(hass, device_data)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
if self.data.event is None:
|
||||
# No tasks, we don't REALLY need to show anything.
|
||||
return {}
|
||||
|
||||
attributes = super().device_state_attributes
|
||||
return attributes
|
||||
|
||||
|
||||
class WebDavCalendarData(object):
|
||||
"""Class to utilize the calendar dav client object to get next event."""
|
||||
|
||||
def __init__(self, calendar, include_all_day, search):
|
||||
"""Set up how we are going to search the WebDav calendar."""
|
||||
self.calendar = calendar
|
||||
self.include_all_day = include_all_day
|
||||
self.search = search
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
# We have to retrieve the results for the whole day as the server
|
||||
# won't return events that have already started
|
||||
results = self.calendar.date_search(
|
||||
dt.start_of_local_day(),
|
||||
dt.start_of_local_day() + timedelta(days=1)
|
||||
)
|
||||
|
||||
# dtstart can be a date or datetime depending if the event lasts a
|
||||
# whole day. Convert everything to datetime to be able to sort it
|
||||
results.sort(key=lambda x: self.to_datetime(
|
||||
x.instance.vevent.dtstart.value
|
||||
))
|
||||
|
||||
vevent = next((
|
||||
event.instance.vevent for event in results
|
||||
if (self.is_matching(event.instance.vevent, self.search)
|
||||
and (not self.is_all_day(event.instance.vevent)
|
||||
or self.include_all_day)
|
||||
and not self.is_over(event.instance.vevent))), None)
|
||||
|
||||
# If no matching event could be found
|
||||
if vevent is None:
|
||||
_LOGGER.debug(
|
||||
"No matching event found in the %d results for %s",
|
||||
len(results),
|
||||
self.calendar.name,
|
||||
)
|
||||
self.event = None
|
||||
return True
|
||||
|
||||
# Populate the entity attributes with the event values
|
||||
self.event = {
|
||||
"summary": vevent.summary.value,
|
||||
"start": self.get_hass_date(vevent.dtstart.value),
|
||||
"end": self.get_hass_date(vevent.dtend.value),
|
||||
"location": self.get_attr_value(vevent, "location"),
|
||||
"description": self.get_attr_value(vevent, "description")
|
||||
}
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_matching(vevent, search):
|
||||
"""Return if the event matches the filter critera."""
|
||||
if search is None:
|
||||
return True
|
||||
|
||||
pattern = re.compile(search)
|
||||
return (hasattr(vevent, "summary")
|
||||
and pattern.match(vevent.summary.value)
|
||||
or hasattr(vevent, "location")
|
||||
and pattern.match(vevent.location.value)
|
||||
or hasattr(vevent, "description")
|
||||
and pattern.match(vevent.description.value))
|
||||
|
||||
@staticmethod
|
||||
def is_all_day(vevent):
|
||||
"""Return if the event last the whole day."""
|
||||
return not isinstance(vevent.dtstart.value, datetime)
|
||||
|
||||
@staticmethod
|
||||
def is_over(vevent):
|
||||
"""Return if the event is over."""
|
||||
return dt.now() > WebDavCalendarData.to_datetime(vevent.dtend.value)
|
||||
|
||||
@staticmethod
|
||||
def get_hass_date(obj):
|
||||
"""Return if the event matches."""
|
||||
if isinstance(obj, datetime):
|
||||
return {"dateTime": obj.isoformat()}
|
||||
|
||||
return {"date": obj.isoformat()}
|
||||
|
||||
@staticmethod
|
||||
def to_datetime(obj):
|
||||
"""Return a datetime."""
|
||||
if isinstance(obj, datetime):
|
||||
return obj
|
||||
return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min))
|
||||
|
||||
@staticmethod
|
||||
def get_attr_value(obj, attribute):
|
||||
"""Return the value of the attribute if defined."""
|
||||
if hasattr(obj, attribute):
|
||||
return getattr(obj, attribute).value
|
||||
return None
|
||||
@@ -8,9 +8,10 @@ import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.amcrest import (
|
||||
STREAM_SOURCE_LIST, TIMEOUT)
|
||||
DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT)
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web,
|
||||
async_aiohttp_proxy_stream)
|
||||
@@ -26,21 +27,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device = discovery_info['device']
|
||||
authentication = discovery_info['authentication']
|
||||
ffmpeg_arguments = discovery_info['ffmpeg_arguments']
|
||||
name = discovery_info['name']
|
||||
resolution = discovery_info['resolution']
|
||||
stream_source = discovery_info['stream_source']
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
async_add_devices([
|
||||
AmcrestCam(hass,
|
||||
name,
|
||||
device,
|
||||
authentication,
|
||||
ffmpeg_arguments,
|
||||
stream_source,
|
||||
resolution)], True)
|
||||
async_add_devices([AmcrestCam(hass, amcrest)], True)
|
||||
|
||||
return True
|
||||
|
||||
@@ -48,18 +38,17 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, name, camera, authentication,
|
||||
ffmpeg_arguments, stream_source, resolution):
|
||||
def __init__(self, hass, amcrest):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = name
|
||||
self._camera = camera
|
||||
self._name = amcrest.name
|
||||
self._camera = amcrest.device
|
||||
self._base_url = self._camera.get_base_url()
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = ffmpeg_arguments
|
||||
self._stream_source = stream_source
|
||||
self._resolution = resolution
|
||||
self._token = self._auth = authentication
|
||||
self._ffmpeg_arguments = amcrest.ffmpeg_arguments
|
||||
self._stream_source = amcrest.stream_source
|
||||
self._resolution = amcrest.resolution
|
||||
self._token = self._auth = amcrest.authentication
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
|
||||
95
homeassistant/components/camera/canary.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Support for Canary camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.canary/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT
|
||||
|
||||
DEPENDENCIES = ['canary']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MOTION_START_TIME = "motion_start_time"
|
||||
ATTR_MOTION_END_TIME = "motion_end_time"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Canary sensors."""
|
||||
data = hass.data[DATA_CANARY]
|
||||
devices = []
|
||||
|
||||
for location in data.locations:
|
||||
entries = data.get_motion_entries(location.location_id)
|
||||
if entries:
|
||||
devices.append(CanaryCamera(data, location.location_id,
|
||||
DEFAULT_TIMEOUT))
|
||||
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class CanaryCamera(Camera):
|
||||
"""An implementation of a Canary security camera."""
|
||||
|
||||
def __init__(self, data, location_id, timeout):
|
||||
"""Initialize a Canary security camera."""
|
||||
super().__init__()
|
||||
self._data = data
|
||||
self._location_id = location_id
|
||||
self._timeout = timeout
|
||||
|
||||
self._location = None
|
||||
self._motion_entry = None
|
||||
self._image_content = None
|
||||
|
||||
def camera_image(self):
|
||||
"""Update the status of the camera and return bytes of camera image."""
|
||||
self.update()
|
||||
return self._image_content
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._location.name
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return self._location.is_recording
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
if self._motion_entry is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
ATTR_MOTION_START_TIME: self._motion_entry.start_time,
|
||||
ATTR_MOTION_END_TIME: self._motion_entry.end_time,
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Update the status of the camera."""
|
||||
self._data.update()
|
||||
self._location = self._data.get_location(self._location_id)
|
||||
|
||||
entries = self._data.get_motion_entries(self._location_id)
|
||||
if entries:
|
||||
current = entries[0]
|
||||
previous = self._motion_entry
|
||||
|
||||
if previous is None or previous.entry_id != current.entry_id:
|
||||
self._motion_entry = current
|
||||
self._image_content = requests.get(
|
||||
current.thumbnails[0].image_url,
|
||||
timeout=self._timeout).content
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return not self._location.is_recording
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
@@ -12,7 +12,8 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.ring import DATA_RING, CONF_ATTRIBUTION
|
||||
from homeassistant.components.ring import (
|
||||
DATA_RING, CONF_ATTRIBUTION, NOTIFICATION_ID)
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL
|
||||
@@ -27,6 +28,8 @@ FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NOTIFICATION_TITLE = 'Ring Camera Setup'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=90)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -42,11 +45,33 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
ring = hass.data[DATA_RING]
|
||||
|
||||
cams = []
|
||||
cams_no_plan = []
|
||||
for camera in ring.doorbells:
|
||||
cams.append(RingCam(hass, camera, config))
|
||||
if camera.has_subscription:
|
||||
cams.append(RingCam(hass, camera, config))
|
||||
else:
|
||||
cams_no_plan.append(camera)
|
||||
|
||||
for camera in ring.stickup_cams:
|
||||
cams.append(RingCam(hass, camera, config))
|
||||
if camera.has_subscription:
|
||||
cams.append(RingCam(hass, camera, config))
|
||||
else:
|
||||
cams_no_plan.append(camera)
|
||||
|
||||
# show notification for all cameras without an active subscription
|
||||
if cams_no_plan:
|
||||
cameras = str(', '.join([camera.name for camera in cams_no_plan]))
|
||||
|
||||
err_msg = '''A Ring Protect Plan is required for the''' \
|
||||
''' following cameras: {}.'''.format(cameras)
|
||||
|
||||
_LOGGER.error(err_msg)
|
||||
hass.components.persistent_notification.async_create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(err_msg),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
|
||||
async_add_devices(cams, True)
|
||||
return True
|
||||
@@ -84,7 +109,6 @@ class RingCam(Camera):
|
||||
'timezone': self._camera.timezone,
|
||||
'type': self._camera.family,
|
||||
'video_url': self._video_url,
|
||||
'video_id': self._last_video_id
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
117
homeassistant/components/canary.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Support for Canary.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/canary/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
from requests import ConnectTimeout, HTTPError
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['py-canary==0.2.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NOTIFICATION_ID = 'canary_notification'
|
||||
NOTIFICATION_TITLE = 'Canary Setup'
|
||||
|
||||
DOMAIN = 'canary'
|
||||
DATA_CANARY = 'canary'
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
CANARY_COMPONENTS = [
|
||||
'alarm_control_panel', 'camera', 'sensor'
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Canary component."""
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
timeout = conf.get(CONF_TIMEOUT)
|
||||
|
||||
try:
|
||||
hass.data[DATA_CANARY] = CanaryData(username, password, timeout)
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Canary service: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
for component in CANARY_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CanaryData(object):
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, username, password, timeout):
|
||||
"""Init the Canary data object."""
|
||||
from canary.api import Api
|
||||
self._api = Api(username, password, timeout)
|
||||
|
||||
self._locations_by_id = {}
|
||||
self._readings_by_device_id = {}
|
||||
self._entries_by_location_id = {}
|
||||
|
||||
self.update()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self, **kwargs):
|
||||
"""Get the latest data from py-canary."""
|
||||
for location in self._api.get_locations():
|
||||
location_id = location.location_id
|
||||
|
||||
self._locations_by_id[location_id] = location
|
||||
self._entries_by_location_id[location_id] = self._api.get_entries(
|
||||
location_id, entry_type="motion", limit=1)
|
||||
|
||||
for device in location.devices:
|
||||
if device.is_online:
|
||||
self._readings_by_device_id[device.device_id] = \
|
||||
self._api.get_latest_readings(device.device_id)
|
||||
|
||||
@property
|
||||
def locations(self):
|
||||
"""Return a list of locations."""
|
||||
return self._locations_by_id.values()
|
||||
|
||||
def get_motion_entries(self, location_id):
|
||||
"""Return a list of motion entries based on location_id."""
|
||||
return self._entries_by_location_id.get(location_id, [])
|
||||
|
||||
def get_location(self, location_id):
|
||||
"""Return a location based on location_id."""
|
||||
return self._locations_by_id.get(location_id, [])
|
||||
|
||||
def get_readings(self, device_id):
|
||||
"""Return a list of readings based on device_id."""
|
||||
return self._readings_by_device_id.get(device_id, [])
|
||||
|
||||
def set_location_mode(self, location_id, mode_name, is_private=False):
|
||||
"""Set location mode."""
|
||||
self._api.set_location_mode(location_id, mode_name, is_private)
|
||||
self.update(no_throttle=True)
|
||||
@@ -51,6 +51,19 @@ STATE_HIGH_DEMAND = 'high_demand'
|
||||
STATE_HEAT_PUMP = 'heat_pump'
|
||||
STATE_GAS = 'gas'
|
||||
|
||||
SUPPORT_TARGET_TEMPERATURE = 1
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH = 2
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW = 4
|
||||
SUPPORT_TARGET_HUMIDITY = 8
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH = 16
|
||||
SUPPORT_TARGET_HUMIDITY_LOW = 32
|
||||
SUPPORT_FAN_MODE = 64
|
||||
SUPPORT_OPERATION_MODE = 128
|
||||
SUPPORT_HOLD_MODE = 256
|
||||
SUPPORT_SWING_MODE = 512
|
||||
SUPPORT_AWAY_MODE = 1024
|
||||
SUPPORT_AUX_HEAT = 2048
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
|
||||
ATTR_MAX_TEMP = 'max_temp'
|
||||
ATTR_MIN_TEMP = 'min_temp'
|
||||
@@ -717,6 +730,11 @@ class ClimateDevice(Entity):
|
||||
"""
|
||||
return self.hass.async_add_job(self.turn_aux_heat_off)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
|
||||
@@ -5,9 +5,19 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW)
|
||||
ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY |
|
||||
SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT |
|
||||
SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo climate devices."""
|
||||
@@ -47,6 +57,11 @@ class DemoClimate(ClimateDevice):
|
||||
self._target_temperature_high = target_temp_high
|
||||
self._target_temperature_low = target_temp_low
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
|
||||
@@ -12,7 +12,9 @@ import voluptuous as vol
|
||||
from homeassistant.components import ecobee
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH)
|
||||
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)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
@@ -44,6 +46,10 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean,
|
||||
})
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
|
||||
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Ecobee Thermostat Platform."""
|
||||
@@ -132,6 +138,11 @@ class Thermostat(ClimateDevice):
|
||||
self.thermostat = self.data.ecobee.get_thermostat(
|
||||
self.thermostat_index)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Ecobee Thermostat."""
|
||||
@@ -318,8 +329,21 @@ class Thermostat(ClimateDevice):
|
||||
|
||||
def set_auto_temp_hold(self, heat_temp, cool_temp):
|
||||
"""Set temperature hold in auto mode."""
|
||||
self.data.ecobee.set_hold_temp(self.thermostat_index, cool_temp,
|
||||
heat_temp, self.hold_preference())
|
||||
if cool_temp is not None:
|
||||
cool_temp_setpoint = cool_temp
|
||||
else:
|
||||
cool_temp_setpoint = (
|
||||
self.thermostat['runtime']['desiredCool'] / 10.0)
|
||||
|
||||
if heat_temp is not None:
|
||||
heat_temp_setpoint = heat_temp
|
||||
else:
|
||||
heat_temp_setpoint = (
|
||||
self.thermostat['runtime']['desiredCool'] / 10.0)
|
||||
|
||||
self.data.ecobee.set_hold_temp(self.thermostat_index,
|
||||
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,
|
||||
@@ -348,8 +372,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 \
|
||||
and 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)
|
||||
@@ -357,6 +381,10 @@ class Thermostat(ClimateDevice):
|
||||
_LOGGER.error(
|
||||
"Missing valid arguments for set_temperature in %s", kwargs)
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set the humidity level."""
|
||||
self.data.ecobee.set_humidity(self.thermostat_index, humidity)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
|
||||
self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode)
|
||||
|
||||
@@ -9,7 +9,7 @@ from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE)
|
||||
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -56,6 +56,11 @@ class EphEmberThermostat(ClimateDevice):
|
||||
self._zone = zone
|
||||
self._hot_water = zone['isHotWater']
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_AUX_HEAT
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the thermostat, if any."""
|
||||
|
||||
@@ -9,7 +9,8 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice)
|
||||
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
|
||||
from homeassistant.const import (
|
||||
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -37,6 +38,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Schema({cv.string: DEVICE_SCHEMA}),
|
||||
})
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the eQ-3 BLE thermostats."""
|
||||
@@ -72,6 +76,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
self._name = _name
|
||||
self._thermostat = eq3.Thermostat(_mac)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if thermostat is available."""
|
||||
|
||||
@@ -17,7 +17,9 @@ import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_SLAVE, TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_FAN_MODE)
|
||||
import homeassistant.components.modbus as modbus
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -31,6 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Flexit Platform."""
|
||||
@@ -62,6 +66,11 @@ class Flexit(ClimateDevice):
|
||||
self._alarm = False
|
||||
self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
def update(self):
|
||||
"""Update unit attributes."""
|
||||
if not self.unit.update():
|
||||
|
||||
@@ -10,17 +10,19 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import switch
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
STATE_AUTO)
|
||||
STATE_AUTO, ATTR_OPERATION_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
|
||||
CONF_NAME)
|
||||
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_time_interval)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,7 +41,8 @@ CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_COLD_TOLERANCE = 'cold_tolerance'
|
||||
CONF_HOT_TOLERANCE = 'hot_tolerance'
|
||||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
|
||||
CONF_INITIAL_OPERATION_MODE = 'initial_operation_mode'
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HEATER): cv.entity_id,
|
||||
@@ -56,6 +59,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_KEEP_ALIVE): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_INITIAL_OPERATION_MODE):
|
||||
vol.In([STATE_AUTO, STATE_OFF])
|
||||
})
|
||||
|
||||
|
||||
@@ -73,11 +78,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
cold_tolerance = config.get(CONF_COLD_TOLERANCE)
|
||||
hot_tolerance = config.get(CONF_HOT_TOLERANCE)
|
||||
keep_alive = config.get(CONF_KEEP_ALIVE)
|
||||
initial_operation_mode = config.get(CONF_INITIAL_OPERATION_MODE)
|
||||
|
||||
async_add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration, cold_tolerance,
|
||||
hot_tolerance, keep_alive)])
|
||||
hot_tolerance, keep_alive, initial_operation_mode)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
@@ -85,7 +91,8 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
cold_tolerance, hot_tolerance, keep_alive):
|
||||
cold_tolerance, hot_tolerance, keep_alive,
|
||||
initial_operation_mode):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@@ -95,7 +102,11 @@ class GenericThermostat(ClimateDevice):
|
||||
self._cold_tolerance = cold_tolerance
|
||||
self._hot_tolerance = hot_tolerance
|
||||
self._keep_alive = keep_alive
|
||||
self._enabled = True
|
||||
self._initial_operation_mode = initial_operation_mode
|
||||
if initial_operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
else:
|
||||
self._enabled = True
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@@ -117,6 +128,23 @@ class GenericThermostat(ClimateDevice):
|
||||
if sensor_state:
|
||||
self._async_update_temp(sensor_state)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
# Check If we have an old state
|
||||
old_state = yield from async_get_last_state(self.hass,
|
||||
self.entity_id)
|
||||
if old_state is not None:
|
||||
# If we have no initial temperature, restore
|
||||
if self._target_temp is None:
|
||||
self._target_temp = float(
|
||||
old_state.attributes[ATTR_TEMPERATURE])
|
||||
|
||||
# If we have no initial operation mode, restore
|
||||
if self._initial_operation_mode is None:
|
||||
if old_state.attributes[ATTR_OPERATION_MODE] == STATE_OFF:
|
||||
self._enabled = False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
@@ -167,7 +195,7 @@ class GenericThermostat(ClimateDevice):
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
self._heater_turn_off()
|
||||
else:
|
||||
_LOGGER.error('Unrecognized operation mode: %s', operation_mode)
|
||||
return
|
||||
@@ -225,9 +253,9 @@ class GenericThermostat(ClimateDevice):
|
||||
def _async_keep_alive(self, time):
|
||||
"""Call at constant intervals for keep-alive purposes."""
|
||||
if self.current_operation in [STATE_COOL, STATE_HEAT]:
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
self._heater_turn_on()
|
||||
else:
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
self._heater_turn_off()
|
||||
|
||||
@callback
|
||||
def _async_update_temp(self, state):
|
||||
@@ -273,13 +301,13 @@ class GenericThermostat(ClimateDevice):
|
||||
self._cold_tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
self._heater_turn_off()
|
||||
else:
|
||||
too_hot = self._cur_temp - self._target_temp >= \
|
||||
self._hot_tolerance
|
||||
if too_hot:
|
||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
self._heater_turn_on()
|
||||
else:
|
||||
is_heating = self._is_device_active
|
||||
if is_heating:
|
||||
@@ -288,15 +316,34 @@ class GenericThermostat(ClimateDevice):
|
||||
if too_hot:
|
||||
_LOGGER.info('Turning off heater %s',
|
||||
self.heater_entity_id)
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
self._heater_turn_off()
|
||||
else:
|
||||
too_cold = self._target_temp - self._cur_temp >= \
|
||||
self._cold_tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
self._heater_turn_on()
|
||||
|
||||
@property
|
||||
def _is_device_active(self):
|
||||
"""If the toggleable device is currently active."""
|
||||
return switch.is_on(self.hass, self.heater_entity_id)
|
||||
return self.hass.states.is_state(self.heater_entity_id, STATE_ON)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@callback
|
||||
def _heater_turn_on(self):
|
||||
"""Turn heater toggleable device on."""
|
||||
data = {ATTR_ENTITY_ID: self.heater_entity_id}
|
||||
self.hass.async_add_job(
|
||||
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data))
|
||||
|
||||
@callback
|
||||
def _heater_turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
data = {ATTR_ENTITY_ID: self.heater_entity_id}
|
||||
self.hass.async_add_job(
|
||||
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data))
|
||||
|
||||
@@ -8,7 +8,8 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -68,6 +69,11 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
self.update()
|
||||
self._target_temperature = int(self.dcb.get('roomset'))
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the thermostat, if any."""
|
||||
|
||||
139
homeassistant/components/climate/hive.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
Support for the Hive devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.hive/
|
||||
"""
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
from homeassistant.components.hive import DATA_HIVE
|
||||
|
||||
DEPENDENCIES = ['hive']
|
||||
HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
|
||||
'ON': STATE_ON, 'OFF': STATE_OFF}
|
||||
HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL',
|
||||
STATE_ON: 'ON', STATE_OFF: 'OFF'}
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Hive climate devices."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
session = hass.data.get(DATA_HIVE)
|
||||
|
||||
add_devices([HiveClimateEntity(session, discovery_info)])
|
||||
|
||||
|
||||
class HiveClimateEntity(ClimateDevice):
|
||||
"""Hive Climate Device."""
|
||||
|
||||
def __init__(self, hivesession, hivedevice):
|
||||
"""Initialize the Climate device."""
|
||||
self.node_id = hivedevice["Hive_NodeID"]
|
||||
self.node_name = hivedevice["Hive_NodeName"]
|
||||
self.device_type = hivedevice["HA_DeviceType"]
|
||||
self.session = hivesession
|
||||
self.data_updatesource = '{}.{}'.format(self.device_type,
|
||||
self.node_id)
|
||||
|
||||
if self.device_type == "Heating":
|
||||
self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF]
|
||||
elif self.device_type == "HotWater":
|
||||
self.modes = [STATE_AUTO, STATE_ON, STATE_OFF]
|
||||
|
||||
self.session.entities.append(self)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
def handle_update(self, updatesource):
|
||||
"""Handle the new update request."""
|
||||
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Climate device."""
|
||||
friendly_name = "Climate Device"
|
||||
if self.device_type == "Heating":
|
||||
friendly_name = "Heating"
|
||||
if self.node_name is not None:
|
||||
friendly_name = '{} {}'.format(self.node_name, friendly_name)
|
||||
elif self.device_type == "HotWater":
|
||||
friendly_name = "Hot Water"
|
||||
return friendly_name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if self.device_type == "Heating":
|
||||
return self.session.heating.current_temperature(self.node_id)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the target temperature."""
|
||||
if self.device_type == "Heating":
|
||||
return self.session.heating.get_target_temperature(self.node_id)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return minimum temperature."""
|
||||
if self.device_type == "Heating":
|
||||
return self.session.heating.min_temperature(self.node_id)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self.device_type == "Heating":
|
||||
return self.session.heating.max_temperature(self.node_id)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of the operation modes."""
|
||||
return self.modes
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current mode."""
|
||||
if self.device_type == "Heating":
|
||||
currentmode = self.session.heating.get_mode(self.node_id)
|
||||
elif self.device_type == "HotWater":
|
||||
currentmode = self.session.hotwater.get_mode(self.node_id)
|
||||
return HIVE_TO_HASS_STATE.get(currentmode)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new Heating mode."""
|
||||
new_mode = HASS_TO_HIVE_STATE.get(operation_mode)
|
||||
if self.device_type == "Heating":
|
||||
self.session.heating.set_mode(self.node_id, new_mode)
|
||||
elif self.device_type == "HotWater":
|
||||
self.session.hotwater.set_mode(self.node_id, new_mode)
|
||||
|
||||
for entity in self.session.entities:
|
||||
entity.handle_update(self.data_updatesource)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
new_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if new_temperature is not None:
|
||||
if self.device_type == "Heating":
|
||||
self.session.heating.set_target_temperature(self.node_id,
|
||||
new_temperature)
|
||||
|
||||
for entity in self.session.entities:
|
||||
entity.handle_update(self.data_updatesource)
|
||||
|
||||
def update(self):
|
||||
"""Update all Node data frome Hive."""
|
||||
self.session.core.update_data(self.node_id)
|
||||
@@ -5,7 +5,9 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.homematic/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||
|
||||
@@ -38,6 +40,8 @@ HM_HUMI_MAP = [
|
||||
|
||||
HM_CONTROL_MODE = 'CONTROL_MODE'
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Homematic thermostat platform."""
|
||||
@@ -55,6 +59,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class HMThermostat(HMDevice, ClimateDevice):
|
||||
"""Representation of a Homematic thermostat."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
|
||||
@@ -14,12 +14,13 @@ import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, ATTR_FAN_MODE, ATTR_FAN_LIST,
|
||||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST)
|
||||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE, CONF_REGION)
|
||||
|
||||
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.4.1']
|
||||
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -126,6 +127,14 @@ class RoundThermostat(ClimateDevice):
|
||||
self._away_temp = away_temp
|
||||
self._away = False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
supported = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE)
|
||||
if hasattr(self.client, ATTR_SYSTEM_MODE):
|
||||
supported |= SUPPORT_OPERATION_MODE
|
||||
return supported
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the honeywell, if any."""
|
||||
@@ -234,6 +243,14 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
supported = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE)
|
||||
if hasattr(self._device, ATTR_SYSTEM_MODE):
|
||||
supported |= SUPPORT_OPERATION_MODE
|
||||
return supported
|
||||
|
||||
@property
|
||||
def is_fan_on(self):
|
||||
"""Return true if fan is on."""
|
||||
|
||||
@@ -8,7 +8,9 @@ import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
|
||||
from homeassistant.components.climate import (
|
||||
PLATFORM_SCHEMA, ClimateDevice, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -135,6 +137,14 @@ class KNXClimate(ClimateDevice):
|
||||
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
support = SUPPORT_TARGET_TEMPERATURE
|
||||
if self.device.supports_operation_mode:
|
||||
support |= SUPPORT_OPERATION_MODE
|
||||
return support
|
||||
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -7,7 +7,9 @@ https://home-assistant.io/components/maxcube/
|
||||
import socket
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
|
||||
@@ -17,6 +19,8 @@ STATE_MANUAL = 'manual'
|
||||
STATE_BOOST = 'boost'
|
||||
STATE_VACATION = 'vacation'
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add thermostats."""
|
||||
@@ -47,6 +51,11 @@ class MaxCubeClimate(ClimateDevice):
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
|
||||
@@ -15,7 +15,9 @@ import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,
|
||||
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO,
|
||||
ATTR_OPERATION_MODE)
|
||||
ATTR_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE,
|
||||
SUPPORT_AUX_HEAT)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
|
||||
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN,
|
||||
@@ -483,3 +485,38 @@ class MqttClimate(ClimateDevice):
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is None:
|
||||
self._aux = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
support = 0
|
||||
|
||||
if (self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None) or \
|
||||
(self._topic[CONF_TEMPERATURE_COMMAND_TOPIC] is not None):
|
||||
support |= SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
if (self._topic[CONF_MODE_COMMAND_TOPIC] is not None) or \
|
||||
(self._topic[CONF_MODE_STATE_TOPIC] is not None):
|
||||
support |= SUPPORT_OPERATION_MODE
|
||||
|
||||
if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or \
|
||||
(self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None):
|
||||
support |= SUPPORT_FAN_MODE
|
||||
|
||||
if (self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None) or \
|
||||
(self._topic[CONF_SWING_MODE_COMMAND_TOPIC] is not None):
|
||||
support |= SUPPORT_SWING_MODE
|
||||
|
||||
if (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) or \
|
||||
(self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None):
|
||||
support |= SUPPORT_AWAY_MODE
|
||||
|
||||
if (self._topic[CONF_HOLD_STATE_TOPIC] is not None) or \
|
||||
(self._topic[CONF_HOLD_COMMAND_TOPIC] is not None):
|
||||
support |= SUPPORT_HOLD_MODE
|
||||
|
||||
if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or \
|
||||
(self._topic[CONF_AUX_COMMAND_TOPIC] is not None):
|
||||
support |= SUPPORT_AUX_HEAT
|
||||
|
||||
return support
|
||||
|
||||
@@ -7,7 +7,9 @@ https://home-assistant.io/components/climate.mysensors/
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO,
|
||||
STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice)
|
||||
STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
|
||||
DICT_HA_TO_MYS = {
|
||||
@@ -23,6 +25,10 @@ DICT_MYS_TO_HA = {
|
||||
'Off': STATE_OFF,
|
||||
}
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors climate."""
|
||||
@@ -33,6 +39,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
"""Representation of a MySensors HVAC."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return True if unable to access real state of entity."""
|
||||
|
||||
@@ -12,7 +12,9 @@ from homeassistant.components.nest import DATA_NEST
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE)
|
||||
ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
@@ -28,6 +30,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
STATE_ECO = 'eco'
|
||||
STATE_HEAT_COOL = 'heat-cool'
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Nest thermostat."""
|
||||
@@ -87,6 +93,11 @@ class NestThermostat(ClimateDevice):
|
||||
self._min_temperature = None
|
||||
self._max_temperature = None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
|
||||
@@ -10,7 +10,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -35,6 +36,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the NetAtmo Thermostat."""
|
||||
@@ -65,6 +69,11 @@ class NetatmoThermostat(ClimateDevice):
|
||||
self._target_temperature = None
|
||||
self._away = None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
|
||||
@@ -14,7 +14,8 @@ import voluptuous as vol
|
||||
|
||||
# Import the device class from the component that you want to support
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE)
|
||||
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE)
|
||||
from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_PORT, TEMP_CELSIUS, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -34,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_AWAY_TEMP, default=14): vol.Coerce(float)
|
||||
})
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the oemthermostat platform."""
|
||||
@@ -77,6 +80,11 @@ class ThermostatDevice(ClimateDevice):
|
||||
self._temperature = None
|
||||
self._setpoint = None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this Thermostat."""
|
||||
|
||||
@@ -8,7 +8,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE,
|
||||
ClimateDevice, PLATFORM_SCHEMA)
|
||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -46,6 +46,11 @@ class ProliphixThermostat(ClimateDevice):
|
||||
self._pdp.update()
|
||||
self._name = self._pdp.name
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Set up polling needed for thermostat."""
|
||||
|
||||
@@ -4,15 +4,18 @@ Support for Radio Thermostat wifi-enabled home thermostats.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.radiotherm/
|
||||
"""
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF,
|
||||
ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_ON, STATE_OFF,
|
||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['radiotherm==1.3']
|
||||
@@ -29,15 +32,56 @@ CONF_AWAY_TEMPERATURE_COOL = 'away_temperature_cool'
|
||||
DEFAULT_AWAY_TEMPERATURE_HEAT = 60
|
||||
DEFAULT_AWAY_TEMPERATURE_COOL = 85
|
||||
|
||||
STATE_CIRCULATE = "circulate"
|
||||
|
||||
OPERATION_LIST = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
|
||||
CT30_FAN_OPERATION_LIST = [STATE_ON, STATE_AUTO]
|
||||
CT80_FAN_OPERATION_LIST = [STATE_ON, STATE_CIRCULATE, STATE_AUTO]
|
||||
|
||||
# Mappings from radiotherm json data codes to and from HASS state
|
||||
# flags. CODE is the thermostat integer code and these map to and
|
||||
# from HASS state flags.
|
||||
|
||||
# Programmed temperature mode of the thermostat.
|
||||
CODE_TO_TEMP_MODE = {0: STATE_OFF, 1: STATE_HEAT, 2: STATE_COOL, 3: STATE_AUTO}
|
||||
TEMP_MODE_TO_CODE = {v: k for k, v in CODE_TO_TEMP_MODE.items()}
|
||||
|
||||
# Programmed fan mode (circulate is supported by CT80 models)
|
||||
CODE_TO_FAN_MODE = {0: STATE_AUTO, 1: STATE_CIRCULATE, 2: STATE_ON}
|
||||
FAN_MODE_TO_CODE = {v: k for k, v in CODE_TO_FAN_MODE.items()}
|
||||
|
||||
# Active thermostat state (is it heating or cooling?). In the future
|
||||
# this should probably made into heat and cool binary sensors.
|
||||
CODE_TO_TEMP_STATE = {0: STATE_IDLE, 1: STATE_HEAT, 2: STATE_COOL}
|
||||
|
||||
# Active fan state. This is if the fan is actually on or not. In the
|
||||
# future this should probably made into a binary sensor for the fan.
|
||||
CODE_TO_FAN_STATE = {0: STATE_OFF, 1: STATE_ON}
|
||||
|
||||
|
||||
def round_temp(temperature):
|
||||
"""Round a temperature to the resolution of the thermostat.
|
||||
|
||||
RadioThermostats can handle 0.5 degree temps so the input
|
||||
temperature is rounded to that value and returned.
|
||||
"""
|
||||
return round(temperature * 2.0) / 2.0
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE_HEAT,
|
||||
default=DEFAULT_AWAY_TEMPERATURE_HEAT): vol.Coerce(float),
|
||||
default=DEFAULT_AWAY_TEMPERATURE_HEAT):
|
||||
vol.All(vol.Coerce(float), round_temp),
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE_COOL,
|
||||
default=DEFAULT_AWAY_TEMPERATURE_COOL): vol.Coerce(float),
|
||||
default=DEFAULT_AWAY_TEMPERATURE_COOL):
|
||||
vol.All(vol.Coerce(float), round_temp),
|
||||
})
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Radio Thermostat."""
|
||||
@@ -77,19 +121,39 @@ class RadioThermostat(ClimateDevice):
|
||||
def __init__(self, device, hold_temp, away_temps):
|
||||
"""Initialize the thermostat."""
|
||||
self.device = device
|
||||
self.set_time()
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_operation = STATE_IDLE
|
||||
self._name = None
|
||||
self._fmode = None
|
||||
self._fstate = None
|
||||
self._tmode = None
|
||||
self._tstate = None
|
||||
self._hold_temp = hold_temp
|
||||
self._hold_set = False
|
||||
self._away = False
|
||||
self._away_temps = away_temps
|
||||
self._prev_temp = None
|
||||
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
|
||||
|
||||
# Fan circulate mode is only supported by the CT80 models.
|
||||
import radiotherm
|
||||
self._is_model_ct80 = isinstance(self.device,
|
||||
radiotherm.thermostat.CT80)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
# Set the time on the device. This shouldn't be in the
|
||||
# constructor because it's a network call. We can't put it in
|
||||
# update() because calling it will clear any temporary mode or
|
||||
# temperature in the thermostat. So add it as a future job
|
||||
# for the event loop to run.
|
||||
self.hass.async_add_job(self.set_time)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -101,6 +165,11 @@ class RadioThermostat(ClimateDevice):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
return PRECISION_HALVES
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
@@ -109,6 +178,25 @@ class RadioThermostat(ClimateDevice):
|
||||
ATTR_MODE: self._tmode,
|
||||
}
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
if self._is_model_ct80:
|
||||
return CT80_FAN_OPERATION_LIST
|
||||
else:
|
||||
return CT30_FAN_OPERATION_LIST
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
return self._fmode
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Turn fan on/off."""
|
||||
code = FAN_MODE_TO_CODE.get(fan, None)
|
||||
if code is not None:
|
||||
self.device.fmode = code
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
@@ -122,7 +210,7 @@ class RadioThermostat(ClimateDevice):
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the operation modes list."""
|
||||
return self._operation_list
|
||||
return OPERATION_LIST
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
@@ -136,53 +224,48 @@ class RadioThermostat(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update and validate the data from the thermostat."""
|
||||
current_temp = self.device.temp['raw']
|
||||
if current_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid temperature reading")
|
||||
return
|
||||
self._current_temperature = current_temp
|
||||
self._name = self.device.name['raw']
|
||||
try:
|
||||
self._fmode = self.device.fmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid fan mode reading")
|
||||
try:
|
||||
self._tmode = self.device.tmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat mode reading")
|
||||
try:
|
||||
self._tstate = self.device.tstate['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat state reading")
|
||||
# Radio thermostats are very slow, and sometimes don't respond
|
||||
# very quickly. So we need to keep the number of calls to them
|
||||
# to a bare minimum or we'll hit the HASS 10 sec warning. We
|
||||
# have to make one call to /tstat to get temps but we'll try and
|
||||
# keep the other calls to a minimum. Even with this, these
|
||||
# thermostats tend to time out sometimes when they're actively
|
||||
# heating or cooling.
|
||||
|
||||
if self._tmode == 'Cool':
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_COOL
|
||||
elif self._tmode == 'Heat':
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_HEAT
|
||||
elif self._tmode == 'Auto':
|
||||
if self._tstate == 'Cool':
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
elif self._tstate == 'Heat':
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_AUTO
|
||||
# First time - get the name from the thermostat. This is
|
||||
# normally set in the radio thermostat web app.
|
||||
if self._name is None:
|
||||
self._name = self.device.name['raw']
|
||||
|
||||
# Request the current state from the thermostat.
|
||||
data = self.device.tstat['raw']
|
||||
|
||||
current_temp = data['temp']
|
||||
if current_temp == -1:
|
||||
_LOGGER.error('%s (%s) was busy (temp == -1)', self._name,
|
||||
self.device.host)
|
||||
return
|
||||
|
||||
# Map thermostat values into various STATE_ flags.
|
||||
self._current_temperature = current_temp
|
||||
self._fmode = CODE_TO_FAN_MODE[data['fmode']]
|
||||
self._fstate = CODE_TO_FAN_STATE[data['fstate']]
|
||||
self._tmode = CODE_TO_TEMP_MODE[data['tmode']]
|
||||
self._tstate = CODE_TO_TEMP_STATE[data['tstate']]
|
||||
|
||||
self._current_operation = self._tmode
|
||||
if self._tmode == STATE_COOL:
|
||||
self._target_temperature = data['t_cool']
|
||||
elif self._tmode == STATE_HEAT:
|
||||
self._target_temperature = data['t_heat']
|
||||
elif self._tmode == STATE_AUTO:
|
||||
# This doesn't really work - tstate is only set if the HVAC is
|
||||
# active. If it's idle, we don't know what to do with the target
|
||||
# temperature.
|
||||
if self._tstate == STATE_COOL:
|
||||
self._target_temperature = data['t_cool']
|
||||
elif self._tstate == STATE_HEAT:
|
||||
self._target_temperature = data['t_heat']
|
||||
else:
|
||||
self._current_operation = STATE_IDLE
|
||||
|
||||
@@ -191,23 +274,32 @@ class RadioThermostat(ClimateDevice):
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
if self._current_operation == STATE_COOL:
|
||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||
elif self._current_operation == STATE_HEAT:
|
||||
self.device.t_heat = round(temperature * 2.0) / 2.0
|
||||
elif self._current_operation == STATE_AUTO:
|
||||
if self._tstate == 'Cool':
|
||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||
elif self._tstate == 'Heat':
|
||||
self.device.t_heat = round(temperature * 2.0) / 2.0
|
||||
|
||||
if self._hold_temp or self._away:
|
||||
self.device.hold = 1
|
||||
else:
|
||||
self.device.hold = 0
|
||||
temperature = round_temp(temperature)
|
||||
|
||||
if self._current_operation == STATE_COOL:
|
||||
self.device.t_cool = temperature
|
||||
elif self._current_operation == STATE_HEAT:
|
||||
self.device.t_heat = temperature
|
||||
elif self._current_operation == STATE_AUTO:
|
||||
if self._tstate == STATE_COOL:
|
||||
self.device.t_cool = temperature
|
||||
elif self._tstate == STATE_HEAT:
|
||||
self.device.t_heat = temperature
|
||||
|
||||
# Only change the hold if requested or if hold mode was turned
|
||||
# on and we haven't set it yet.
|
||||
if kwargs.get('hold_changed', False) or not self._hold_set:
|
||||
if self._hold_temp or self._away:
|
||||
self.device.hold = 1
|
||||
self._hold_set = True
|
||||
else:
|
||||
self.device.hold = 0
|
||||
|
||||
def set_time(self):
|
||||
"""Set device time."""
|
||||
# Calling this clears any local temperature override and
|
||||
# reverts to the scheduled temperature.
|
||||
now = datetime.datetime.now()
|
||||
self.device.time = {
|
||||
'day': now.weekday(),
|
||||
@@ -217,14 +309,14 @@ class RadioThermostat(ClimateDevice):
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode (auto, cool, heat, off)."""
|
||||
if operation_mode == STATE_OFF:
|
||||
self.device.tmode = 0
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.device.tmode = 3
|
||||
if operation_mode == STATE_OFF or operation_mode == STATE_AUTO:
|
||||
self.device.tmode = TEMP_MODE_TO_CODE[operation_mode]
|
||||
|
||||
# Setting t_cool or t_heat automatically changes tmode.
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0
|
||||
self.device.t_cool = self._target_temperature
|
||||
elif operation_mode == STATE_HEAT:
|
||||
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0
|
||||
self.device.t_heat = self._target_temperature
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on.
|
||||
@@ -238,10 +330,11 @@ class RadioThermostat(ClimateDevice):
|
||||
away_temp = self._away_temps[0]
|
||||
elif self._current_operation == STATE_COOL:
|
||||
away_temp = self._away_temps[1]
|
||||
|
||||
self._away = True
|
||||
self.set_temperature(temperature=away_temp)
|
||||
self.set_temperature(temperature=away_temp, hold_changed=True)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self._away = False
|
||||
self.set_temperature(temperature=self._prev_temp)
|
||||
self.set_temperature(temperature=self._prev_temp, hold_changed=True)
|
||||
|
||||
@@ -15,7 +15,10 @@ import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA)
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_SWING_MODE,
|
||||
SUPPORT_AUX_HEAT)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -35,9 +38,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
_FETCH_FIELDS = ','.join([
|
||||
'room{name}', 'measurements', 'remoteCapabilities',
|
||||
'acState', 'connectionStatus{isAlive}'])
|
||||
'acState', 'connectionStatus{isAlive}', 'temperatureUnit'])
|
||||
_INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_SWING_MODE |
|
||||
SUPPORT_AUX_HEAT)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
@@ -55,7 +62,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
devices.append(SensiboClimate(client, dev))
|
||||
except (aiohttp.client_exceptions.ClientConnectorError,
|
||||
asyncio.TimeoutError):
|
||||
_LOGGER.exception('Failed to connct to Sensibo servers.')
|
||||
_LOGGER.exception('Failed to connect to Sensibo servers.')
|
||||
raise PlatformNotReady
|
||||
|
||||
if devices:
|
||||
@@ -63,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class SensiboClimate(ClimateDevice):
|
||||
"""Representation os a Sensibo device."""
|
||||
"""Representation of a Sensibo device."""
|
||||
|
||||
def __init__(self, client, data):
|
||||
"""Build SensiboClimate.
|
||||
@@ -75,6 +82,11 @@ class SensiboClimate(ClimateDevice):
|
||||
self._id = data['id']
|
||||
self._do_update(data)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
def _do_update(self, data):
|
||||
self._name = data['room']['name']
|
||||
self._measurements = data['measurements']
|
||||
@@ -84,11 +96,16 @@ class SensiboClimate(ClimateDevice):
|
||||
self._operations = sorted(capabilities['modes'].keys())
|
||||
self._current_capabilities = capabilities[
|
||||
'modes'][self.current_operation]
|
||||
temperature_unit_key = self._ac_states['temperatureUnit']
|
||||
self._temperature_unit = \
|
||||
TEMP_CELSIUS if temperature_unit_key == 'C' else TEMP_FAHRENHEIT
|
||||
self._temperatures_list = self._current_capabilities[
|
||||
'temperatures'][temperature_unit_key]['values']
|
||||
temperature_unit_key = data.get('temperatureUnit') or \
|
||||
self._ac_states.get('temperatureUnit')
|
||||
if temperature_unit_key:
|
||||
self._temperature_unit = TEMP_CELSIUS if \
|
||||
temperature_unit_key == 'C' else TEMP_FAHRENHEIT
|
||||
self._temperatures_list = self._current_capabilities[
|
||||
'temperatures'].get(temperature_unit_key, {}).get('values', [])
|
||||
else:
|
||||
self._temperature_unit = self.unit_of_measurement
|
||||
self._temperatures_list = []
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -108,7 +125,7 @@ class SensiboClimate(ClimateDevice):
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._ac_states['targetTemperature']
|
||||
return self._ac_states.get('targetTemperature')
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
@@ -133,10 +150,8 @@ class SensiboClimate(ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
# This field is not affected by temperature_unit.
|
||||
# It is always in C / nativeTemperatureUnit
|
||||
if 'nativeTemperatureUnit' not in self._ac_states:
|
||||
return self._measurements['temperature']
|
||||
# This field is not affected by temperatureUnit.
|
||||
# It is always in C
|
||||
return convert_temperature(
|
||||
self._measurements['temperature'],
|
||||
TEMP_CELSIUS,
|
||||
@@ -180,12 +195,14 @@ class SensiboClimate(ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._temperatures_list[0]
|
||||
return self._temperatures_list[0] \
|
||||
if len(self._temperatures_list) else super.min_temp()
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._temperatures_list[-1]
|
||||
return self._temperatures_list[-1] \
|
||||
if len(self._temperatures_list) else super.max_temp()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
|
||||
@@ -7,7 +7,8 @@ https://home-assistant.io/components/climate.tado/
|
||||
import logging
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.components.tado import DATA_TADO
|
||||
|
||||
@@ -43,6 +44,8 @@ OPERATION_LIST = {
|
||||
CONST_MODE_OFF: 'Off',
|
||||
}
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tado climate platform."""
|
||||
@@ -56,8 +59,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
climate_devices.append(create_climate_device(
|
||||
tado, hass, zone, zone['name'], zone['id']))
|
||||
device = create_climate_device(
|
||||
tado, hass, zone, zone['name'], zone['id'])
|
||||
if not device:
|
||||
continue
|
||||
climate_devices.append(device)
|
||||
|
||||
if climate_devices:
|
||||
add_devices(climate_devices, True)
|
||||
@@ -72,8 +78,11 @@ def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
|
||||
if ac_mode:
|
||||
temperatures = capabilities['HEAT']['temperatures']
|
||||
else:
|
||||
elif 'temperatures' in capabilities:
|
||||
temperatures = capabilities['temperatures']
|
||||
else:
|
||||
_LOGGER.debug("Received zone %s has no temperature; not adding", name)
|
||||
return
|
||||
|
||||
min_temp = float(temperatures['celsius']['min'])
|
||||
max_temp = float(temperatures['celsius']['max'])
|
||||
@@ -127,6 +136,11 @@ class TadoClimate(ClimateDevice):
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
|
||||
@@ -7,7 +7,9 @@ https://home-assistant.io/components/climate.tesla/
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE)
|
||||
@@ -18,6 +20,8 @@ DEPENDENCIES = ['tesla']
|
||||
|
||||
OPERATION_LIST = [STATE_ON, STATE_OFF]
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tesla climate platform."""
|
||||
@@ -36,6 +40,11 @@ class TeslaThermostat(TeslaDevice, ClimateDevice):
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. On or Off."""
|
||||
|
||||
@@ -10,9 +10,11 @@ https://home-assistant.io/components/climate.toon/
|
||||
import homeassistant.components.toon as toon_main
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ATTR_TEMPERATURE, STATE_PERFORMANCE, STATE_HEAT, STATE_ECO,
|
||||
STATE_COOL)
|
||||
STATE_COOL, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Toon thermostat."""
|
||||
@@ -38,6 +40,11 @@ class ThermostatDevice(ClimateDevice):
|
||||
STATE_COOL,
|
||||
]
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of this Thermostat."""
|
||||
|
||||
@@ -7,7 +7,9 @@ https://home-assistant.io/components/switch.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE)
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
@@ -23,12 +25,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||
OPERATION_LIST = ['Heat', 'Cool', 'Auto Changeover', 'Off']
|
||||
FAN_OPERATION_LIST = ['On', 'Auto', 'Cycle']
|
||||
|
||||
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_FAN_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up of Vera thermostats."""
|
||||
add_devices_callback(
|
||||
VeraThermostat(device, VERA_CONTROLLER) for
|
||||
device in VERA_DEVICES['climate'])
|
||||
VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
|
||||
device in hass.data[VERA_DEVICES]['climate'])
|
||||
|
||||
|
||||
class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
@@ -39,6 +44,11 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
|
||||
@@ -11,7 +11,10 @@ from homeassistant.components.climate import (
|
||||
STATE_ECO, STATE_GAS, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ELECTRIC,
|
||||
STATE_FAN_ONLY, STATE_HEAT_PUMP, ATTR_TEMPERATURE, STATE_HIGH_DEMAND,
|
||||
STATE_PERFORMANCE, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_TARGET_TEMP_HIGH, ClimateDevice)
|
||||
ATTR_TARGET_TEMP_HIGH, ClimateDevice, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_AUX_HEAT)
|
||||
from homeassistant.components.wink import DOMAIN, WinkDevice
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, TEMP_CELSIUS, STATE_UNKNOWN, PRECISION_TENTHS)
|
||||
@@ -50,6 +53,17 @@ HA_STATE_TO_WINK = {
|
||||
|
||||
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
|
||||
|
||||
SUPPORT_FLAGS_THERMOSTAT = (
|
||||
SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT)
|
||||
|
||||
SUPPORT_FLAGS_AC = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_FAN_MODE)
|
||||
|
||||
SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_AWAY_MODE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Wink climate devices."""
|
||||
@@ -72,6 +86,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink thermostat."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS_THERMOSTAT
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
@@ -353,6 +372,11 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
class WinkAC(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink air conditioner."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS_AC
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
@@ -471,6 +495,11 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
class WinkWaterHeater(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink water heater."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS_HEATER
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
|
||||
@@ -7,8 +7,9 @@ https://home-assistant.io/components/climate.zwave/
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.const import (
|
||||
@@ -70,6 +71,18 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._zxt_120 = 1
|
||||
self.update_properties()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
support = SUPPORT_TARGET_TEMPERATURE
|
||||
if self.values.fan_mode:
|
||||
support |= SUPPORT_FAN_MODE
|
||||
if self.values.mode:
|
||||
support |= SUPPORT_OPERATION_MODE
|
||||
if self._zxt_120 == 1 and self.values.zxt_120_swing_mode:
|
||||
support |= SUPPORT_SWING_MODE
|
||||
return support
|
||||
|
||||
def update_properties(self):
|
||||
"""Handle the data changes for node values."""
|
||||
# Operation Mode
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.components.alexa import smart_home
|
||||
from . import http_api, iot
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
|
||||
REQUIREMENTS = ['warrant==0.5.0']
|
||||
REQUIREMENTS = ['warrant==0.6.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,7 +27,7 @@ CONF_RELAYER = 'relayer'
|
||||
CONF_USER_POOL_ID = 'user_pool_id'
|
||||
|
||||
MODE_DEV = 'development'
|
||||
DEFAULT_MODE = MODE_DEV
|
||||
DEFAULT_MODE = 'production'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ALEXA_SCHEMA = vol.Schema({
|
||||
@@ -42,10 +42,10 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
|
||||
vol.In([MODE_DEV] + list(SERVERS)),
|
||||
# Change to optional when we include real servers
|
||||
vol.Required(CONF_COGNITO_CLIENT_ID): str,
|
||||
vol.Required(CONF_USER_POOL_ID): str,
|
||||
vol.Required(CONF_REGION): str,
|
||||
vol.Required(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_COGNITO_CLIENT_ID): str,
|
||||
vol.Optional(CONF_USER_POOL_ID): str,
|
||||
vol.Optional(CONF_REGION): str,
|
||||
vol.Optional(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@@ -104,6 +104,11 @@ class Cloud:
|
||||
self.region = info['region']
|
||||
self.relayer = info['relayer']
|
||||
|
||||
@property
|
||||
def cognito_email_based(self):
|
||||
"""Return if cognito is email based."""
|
||||
return not self.user_pool_id.endswith('GmV')
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
"""Get if cloud is logged in."""
|
||||
@@ -112,10 +117,6 @@ class Cloud:
|
||||
@property
|
||||
def subscription_expired(self):
|
||||
"""Return a boolen if the subscription has expired."""
|
||||
# For now, don't enforce subscriptions to exist
|
||||
if 'custom:sub-exp' not in self.claims:
|
||||
return False
|
||||
|
||||
return dt_util.utcnow() > self.expiration_date
|
||||
|
||||
@property
|
||||
|
||||
@@ -68,8 +68,14 @@ def register(cloud, email, password):
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(cloud)
|
||||
# Workaround for bug in Warrant. PR with fix:
|
||||
# https://github.com/capless/warrant/pull/82
|
||||
cognito.add_base_attributes()
|
||||
try:
|
||||
cognito.register(_generate_username(email), password, email=email)
|
||||
if cloud.cognito_email_based:
|
||||
cognito.register(email, password)
|
||||
else:
|
||||
cognito.register(_generate_username(email), password)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
@@ -80,7 +86,11 @@ def confirm_register(cloud, confirmation_code, email):
|
||||
|
||||
cognito = _cognito(cloud)
|
||||
try:
|
||||
cognito.confirm_sign_up(confirmation_code, _generate_username(email))
|
||||
if cloud.cognito_email_based:
|
||||
cognito.confirm_sign_up(confirmation_code, email)
|
||||
else:
|
||||
cognito.confirm_sign_up(confirmation_code,
|
||||
_generate_username(email))
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
@@ -89,7 +99,11 @@ def forgot_password(cloud, email):
|
||||
"""Initiate forgotten password flow."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
if cloud.cognito_email_based:
|
||||
cognito = _cognito(cloud, username=email)
|
||||
else:
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
|
||||
try:
|
||||
cognito.initiate_forgot_password()
|
||||
except ClientError as err:
|
||||
@@ -100,7 +114,11 @@ def confirm_forgot_password(cloud, confirmation_code, email, new_password):
|
||||
"""Confirm forgotten password code and change password."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
if cloud.cognito_email_based:
|
||||
cognito = _cognito(cloud, username=email)
|
||||
else:
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
|
||||
try:
|
||||
cognito.confirm_forgot_password(confirmation_code, new_password)
|
||||
except ClientError as err:
|
||||
|
||||
@@ -4,13 +4,12 @@ CONFIG_DIR = '.cloud'
|
||||
REQUEST_TIMEOUT = 10
|
||||
|
||||
SERVERS = {
|
||||
# Example entry:
|
||||
# 'production': {
|
||||
# 'cognito_client_id': '',
|
||||
# 'user_pool_id': '',
|
||||
# 'region': '',
|
||||
# 'relayer': ''
|
||||
# }
|
||||
'production': {
|
||||
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
|
||||
'user_pool_id': 'us-east-1_87ll5WOP8',
|
||||
'region': 'us-east-1',
|
||||
'relayer': 'wss://cloud.hass.io:8000/websocket'
|
||||
}
|
||||
}
|
||||
|
||||
MESSAGE_EXPIRATION = """
|
||||
|
||||
@@ -65,12 +65,12 @@ class CloudLoginView(HomeAssistantView):
|
||||
url = '/api/cloud/login'
|
||||
name = 'api:cloud:login'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
vol.Required('password'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle login request."""
|
||||
hass = request.app['hass']
|
||||
@@ -92,8 +92,8 @@ class CloudLogoutView(HomeAssistantView):
|
||||
url = '/api/cloud/logout'
|
||||
name = 'api:cloud:logout'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle logout request."""
|
||||
hass = request.app['hass']
|
||||
@@ -129,12 +129,12 @@ class CloudRegisterView(HomeAssistantView):
|
||||
url = '/api/cloud/register'
|
||||
name = 'api:cloud:register'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
vol.Required('password'): vol.All(str, vol.Length(min=6)),
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle registration request."""
|
||||
hass = request.app['hass']
|
||||
@@ -153,12 +153,12 @@ class CloudConfirmRegisterView(HomeAssistantView):
|
||||
url = '/api/cloud/confirm_register'
|
||||
name = 'api:cloud:confirm_register'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('confirmation_code'): str,
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle registration confirmation request."""
|
||||
hass = request.app['hass']
|
||||
@@ -178,11 +178,11 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||
url = '/api/cloud/forgot_password'
|
||||
name = 'api:cloud:forgot_password'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password request."""
|
||||
hass = request.app['hass']
|
||||
@@ -201,13 +201,13 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
|
||||
url = '/api/cloud/confirm_forgot_password'
|
||||
name = 'api:cloud:confirm_forgot_password'
|
||||
|
||||
@asyncio.coroutine
|
||||
@_handle_cloud_errors
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('confirmation_code'): str,
|
||||
vol.Required('email'): str,
|
||||
vol.Required('new_password'): vol.All(str, vol.Length(min=6))
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password confirm request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
@@ -59,13 +59,6 @@ class CloudIoT:
|
||||
if self.state == STATE_CONNECTED:
|
||||
raise RuntimeError('Already connected')
|
||||
|
||||
self.state = STATE_CONNECTING
|
||||
self.close_requested = False
|
||||
remove_hass_stop_listener = None
|
||||
session = async_get_clientsession(self.cloud.hass)
|
||||
client = None
|
||||
disconnect_warn = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def _handle_hass_stop(event):
|
||||
"""Handle Home Assistant shutting down."""
|
||||
@@ -73,6 +66,14 @@ class CloudIoT:
|
||||
remove_hass_stop_listener = None
|
||||
yield from self.disconnect()
|
||||
|
||||
self.state = STATE_CONNECTING
|
||||
self.close_requested = False
|
||||
remove_hass_stop_listener = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
|
||||
session = async_get_clientsession(self.cloud.hass)
|
||||
client = None
|
||||
disconnect_warn = None
|
||||
|
||||
try:
|
||||
yield from hass.async_add_job(auth_api.check_token, self.cloud)
|
||||
|
||||
@@ -83,9 +84,6 @@ class CloudIoT:
|
||||
})
|
||||
self.tries = 0
|
||||
|
||||
remove_hass_stop_listener = hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
|
||||
|
||||
_LOGGER.info('Connected')
|
||||
self.state = STATE_CONNECTED
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Provide configuration end points for Z-Wave."""
|
||||
"""Provide configuration end points for Automations."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.config import EditIdBasedConfigView
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Provide configuration end points for Groups."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.components.config import EditKeyBasedConfigView
|
||||
from homeassistant.components.group import GROUP_SCHEMA
|
||||
from homeassistant.components.group import DOMAIN, GROUP_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ CONFIG_PATH = 'groups.yaml'
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Set up the Group config API."""
|
||||
@asyncio.coroutine
|
||||
def hook(hass):
|
||||
"""post_write_hook for Config View that reloads groups."""
|
||||
yield from hass.services.async_call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
hass.http.register_view(EditKeyBasedConfigView(
|
||||
'group', 'config', CONFIG_PATH, cv.slug, GROUP_SCHEMA
|
||||
'group', 'config', CONFIG_PATH, cv.slug, GROUP_SCHEMA,
|
||||
post_write_hook=hook
|
||||
))
|
||||
return True
|
||||
|
||||
@@ -50,15 +50,19 @@ def async_request_config(
|
||||
|
||||
Will return an ID to be used for sequent calls.
|
||||
"""
|
||||
if link_name is not None and link_url is not None:
|
||||
description += '\n\n[{}]({})'.format(link_name, link_url)
|
||||
|
||||
if description_image is not None:
|
||||
description += '\n\n'.format(description_image)
|
||||
|
||||
instance = hass.data.get(_KEY_INSTANCE)
|
||||
|
||||
if instance is None:
|
||||
instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
|
||||
|
||||
request_id = instance.async_request_config(
|
||||
name, callback,
|
||||
description, description_image, submit_caption,
|
||||
fields, link_name, link_url, entity_picture)
|
||||
name, callback, description, submit_caption, fields, entity_picture)
|
||||
|
||||
if DATA_REQUESTS not in hass.data:
|
||||
hass.data[DATA_REQUESTS] = {}
|
||||
@@ -137,9 +141,8 @@ class Configurator(object):
|
||||
|
||||
@async_callback
|
||||
def async_request_config(
|
||||
self, name, callback,
|
||||
description, description_image, submit_caption,
|
||||
fields, link_name, link_url, entity_picture):
|
||||
self, name, callback, description, submit_caption, fields,
|
||||
entity_picture):
|
||||
"""Set up a request for configuration."""
|
||||
entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, name, hass=self.hass)
|
||||
@@ -161,10 +164,7 @@ class Configurator(object):
|
||||
data.update({
|
||||
key: value for key, value in [
|
||||
(ATTR_DESCRIPTION, description),
|
||||
(ATTR_DESCRIPTION_IMAGE, description_image),
|
||||
(ATTR_SUBMIT_CAPTION, submit_caption),
|
||||
(ATTR_LINK_NAME, link_name),
|
||||
(ATTR_LINK_URL, link_url),
|
||||
] if value is not None
|
||||
})
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
from homeassistant import core
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, HTTP_BAD_REQUEST)
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
from homeassistant.helpers import intent, config_validation as cv
|
||||
from homeassistant.components import http
|
||||
|
||||
@@ -39,6 +39,10 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
|
||||
})
|
||||
})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
INTENT_TURN_ON = 'HassTurnOn'
|
||||
INTENT_TURN_OFF = 'HassTurnOff'
|
||||
REGEX_TYPE = type(re.compile(''))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -60,7 +64,11 @@ def async_register(hass, intent_type, utterances):
|
||||
if conf is None:
|
||||
conf = intents[intent_type] = []
|
||||
|
||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
||||
for utterance in utterances:
|
||||
if isinstance(utterance, REGEX_TYPE):
|
||||
conf.append(utterance)
|
||||
else:
|
||||
conf.append(_create_matcher(utterance))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -93,6 +101,13 @@ def async_setup(hass, config):
|
||||
|
||||
hass.http.register_view(ConversationProcessView)
|
||||
|
||||
hass.helpers.intent.async_register(TurnOnIntent())
|
||||
hass.helpers.intent.async_register(TurnOffIntent())
|
||||
async_register(hass, INTENT_TURN_ON,
|
||||
['Turn {name} on', 'Turn on {name}'])
|
||||
async_register(hass, INTENT_TURN_OFF, [
|
||||
'Turn {name} off', 'Turn off {name}'])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -128,48 +143,84 @@ def _process(hass, text):
|
||||
if not match:
|
||||
continue
|
||||
|
||||
response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_type,
|
||||
response = yield from hass.helpers.intent.async_handle(
|
||||
DOMAIN, intent_type,
|
||||
{key: {'value': value} for key, value
|
||||
in match.groupdict().items()}, text)
|
||||
return response
|
||||
|
||||
|
||||
@core.callback
|
||||
def _match_entity(hass, name):
|
||||
"""Match a name to an entity."""
|
||||
from fuzzywuzzy import process as fuzzyExtract
|
||||
text = text.lower()
|
||||
match = REGEX_TURN_COMMAND.match(text)
|
||||
|
||||
if not match:
|
||||
_LOGGER.error("Unable to process: %s", text)
|
||||
return None
|
||||
|
||||
name, command = match.groups()
|
||||
entities = {state.entity_id: state.name for state
|
||||
in hass.states.async_all()}
|
||||
entity_ids = fuzzyExtract.extractOne(
|
||||
entity_id = fuzzyExtract.extractOne(
|
||||
name, entities, score_cutoff=65)[2]
|
||||
return hass.states.get(entity_id) if entity_id else None
|
||||
|
||||
if not entity_ids:
|
||||
_LOGGER.error(
|
||||
"Could not find entity id %s from text %s", name, text)
|
||||
return None
|
||||
|
||||
if command == 'on':
|
||||
class TurnOnIntent(intent.IntentHandler):
|
||||
"""Handle turning item on intents."""
|
||||
|
||||
intent_type = INTENT_TURN_ON
|
||||
slot_schema = {
|
||||
'name': cv.string,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle(self, intent_obj):
|
||||
"""Handle turn on intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
name = slots['name']['value']
|
||||
entity = _match_entity(hass, name)
|
||||
|
||||
if not entity:
|
||||
_LOGGER.error("Could not find entity id for %s", name)
|
||||
return None
|
||||
|
||||
yield from hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
}, blocking=True)
|
||||
|
||||
elif command == 'off':
|
||||
response = intent_obj.create_response()
|
||||
response.async_set_speech(
|
||||
'Turned on {}'.format(entity.name))
|
||||
return response
|
||||
|
||||
|
||||
class TurnOffIntent(intent.IntentHandler):
|
||||
"""Handle turning item off intents."""
|
||||
|
||||
intent_type = INTENT_TURN_OFF
|
||||
slot_schema = {
|
||||
'name': cv.string,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle(self, intent_obj):
|
||||
"""Handle turn off intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
name = slots['name']['value']
|
||||
entity = _match_entity(hass, name)
|
||||
|
||||
if not entity:
|
||||
_LOGGER.error("Could not find entity id for %s", name)
|
||||
return None
|
||||
|
||||
yield from hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
}, blocking=True)
|
||||
|
||||
else:
|
||||
_LOGGER.error('Got unsupported command %s from text %s',
|
||||
command, text)
|
||||
|
||||
return None
|
||||
response = intent_obj.create_response()
|
||||
response.async_set_speech(
|
||||
'Turned off {}'.format(entity.name))
|
||||
return response
|
||||
|
||||
|
||||
class ConversationProcessView(http.HomeAssistantView):
|
||||
@@ -178,23 +229,15 @@ class ConversationProcessView(http.HomeAssistantView):
|
||||
url = '/api/conversation/process'
|
||||
name = "api:conversation:process"
|
||||
|
||||
@http.RequestDataValidator(vol.Schema({
|
||||
vol.Required('text'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
def post(self, request, data):
|
||||
"""Send a request for processing."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
text = data.get('text')
|
||||
|
||||
if text is None:
|
||||
return self.json_message('Missing "text" key in JSON.',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
intent_result = yield from _process(hass, text)
|
||||
intent_result = yield from _process(hass, data['text'])
|
||||
|
||||
if intent_result is None:
|
||||
intent_result = intent.IntentResponse()
|
||||
|
||||
@@ -69,7 +69,10 @@ class ISYCoverDevice(isy.ISYDevice, CoverDevice):
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 cover device."""
|
||||
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
|
||||
if self.is_unknown():
|
||||
return None
|
||||
else:
|
||||
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
|
||||
|
||||
def open_cover(self, **kwargs) -> None:
|
||||
"""Send the open cover command to the ISY994 cover device."""
|
||||
|
||||
73
homeassistant/components/cover/tahoma.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Support for Tahoma cover - shutters etc.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.tahoma/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.tahoma import (
|
||||
DOMAIN as TAHOMA_DOMAIN, TahomaDevice)
|
||||
|
||||
DEPENDENCIES = ['tahoma']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Tahoma covers."""
|
||||
controller = hass.data[TAHOMA_DOMAIN]['controller']
|
||||
devices = []
|
||||
for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']:
|
||||
devices.append(TahomaCover(device, controller))
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class TahomaCover(TahomaDevice, CoverDevice):
|
||||
"""Representation a Tahoma Cover."""
|
||||
|
||||
def __init__(self, tahoma_device, controller):
|
||||
"""Initialize the Tahoma device."""
|
||||
super().__init__(tahoma_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id)
|
||||
|
||||
def update(self):
|
||||
"""Update method."""
|
||||
self.controller.get_states([self.tahoma_device])
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""
|
||||
Return current position of cover.
|
||||
|
||||
0 is closed, 100 is fully open.
|
||||
"""
|
||||
position = 100 - self.tahoma_device.active_states['core:ClosureState']
|
||||
if position <= 5:
|
||||
return 0
|
||||
if position >= 95:
|
||||
return 100
|
||||
return position
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
self.apply_action('setPosition', 100 - position)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
return self.current_cover_position == 0
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self.apply_action('open')
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self.apply_action('close')
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self.apply_action('stopIdentify')
|
||||
65
homeassistant/components/cover/tellstick.py
Executable file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Support for Tellstick covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.tellstick/
|
||||
"""
|
||||
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.tellstick import (
|
||||
DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG,
|
||||
DATA_TELLSTICK, TellstickDevice)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tellstick covers."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
return
|
||||
|
||||
signal_repetitions = discovery_info.get(
|
||||
ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS)
|
||||
|
||||
add_devices([TellstickCover(hass.data[DATA_TELLSTICK][tellcore_id],
|
||||
signal_repetitions)
|
||||
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]],
|
||||
True)
|
||||
|
||||
|
||||
class TellstickCover(TellstickDevice, CoverDevice):
|
||||
"""Representation of a Tellstick cover."""
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of the cover is not possible."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return True if unable to access real state of the entity."""
|
||||
return True
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._tellcore_device.down()
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._tellcore_device.up()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._tellcore_device.stop()
|
||||
|
||||
def _parse_tellcore_data(self, tellcore_data):
|
||||
"""Turn the value received from tellcore into something useful."""
|
||||
pass
|
||||
|
||||
def _parse_ha_data(self, kwargs):
|
||||
"""Turn the value from HA into something useful."""
|
||||
pass
|
||||
|
||||
def _update_model(self, new_state, data):
|
||||
"""Update the device entity state to match the arguments."""
|
||||
pass
|
||||
@@ -18,8 +18,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Vera covers."""
|
||||
add_devices(
|
||||
VeraCover(device, VERA_CONTROLLER) for
|
||||
device in VERA_DEVICES['cover'])
|
||||
VeraCover(device, hass.data[VERA_CONTROLLER]) for
|
||||
device in hass.data[VERA_DEVICES]['cover'])
|
||||
|
||||
|
||||
class VeraCover(VeraDevice, CoverDevice):
|
||||
|
||||
@@ -53,6 +53,7 @@ YAML_DEVICES = 'known_devices.yaml'
|
||||
|
||||
CONF_TRACK_NEW = 'track_new_devices'
|
||||
DEFAULT_TRACK_NEW = True
|
||||
CONF_NEW_DEVICE_DEFAULTS = 'new_device_defaults'
|
||||
|
||||
CONF_CONSIDER_HOME = 'consider_home'
|
||||
DEFAULT_CONSIDER_HOME = timedelta(seconds=180)
|
||||
@@ -81,12 +82,18 @@ ATTR_VENDOR = 'vendor'
|
||||
SOURCE_TYPE_GPS = 'gps'
|
||||
SOURCE_TYPE_ROUTER = 'router'
|
||||
|
||||
NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
|
||||
}))
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_CONSIDER_HOME,
|
||||
default=DEFAULT_CONSIDER_HOME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_NEW_DEVICE_DEFAULTS,
|
||||
default={}): NEW_DEVICE_DEFAULTS_SCHEMA
|
||||
})
|
||||
|
||||
|
||||
@@ -125,9 +132,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
conf = conf[0] if conf else {}
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {})
|
||||
|
||||
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
||||
tracker = DeviceTracker(
|
||||
hass, consider_home, track_new, defaults, devices)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(p_type, p_config, disc_info=None):
|
||||
@@ -211,13 +220,15 @@ class DeviceTracker(object):
|
||||
"""Representation of a device tracker."""
|
||||
|
||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track_new: bool, devices: Sequence) -> None:
|
||||
track_new: bool, defaults: dict,
|
||||
devices: Sequence) -> None:
|
||||
"""Initialize a device tracker."""
|
||||
self.hass = hass
|
||||
self.devices = {dev.dev_id: dev for dev in devices}
|
||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new
|
||||
self.track_new = defaults.get(CONF_TRACK_NEW, track_new)
|
||||
self.defaults = defaults
|
||||
self.group = None
|
||||
self._is_updating = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
@@ -274,7 +285,8 @@ class DeviceTracker(object):
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '),
|
||||
picture=picture, icon=icon)
|
||||
picture=picture, icon=icon,
|
||||
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
@@ -11,7 +11,8 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL)
|
||||
|
||||
@@ -38,7 +39,7 @@ def get_scanner(hass, config):
|
||||
return None
|
||||
|
||||
|
||||
class LinksysAPDeviceScanner(object):
|
||||
class LinksysAPDeviceScanner(DeviceScanner):
|
||||
"""This class queries a Linksys Access Point."""
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
136
homeassistant/components/device_tracker/meraki.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Support for the Meraki CMX location service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.meraki/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE_ENTITY)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER)
|
||||
|
||||
CONF_VALIDATOR = 'validator'
|
||||
CONF_SECRET = 'secret'
|
||||
DEPENDENCIES = ['http']
|
||||
URL = '/api/meraki'
|
||||
VERSION = '2.0'
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_VALIDATOR): cv.string,
|
||||
vol.Required(CONF_SECRET): cv.string
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up an endpoint for the Meraki tracker."""
|
||||
hass.http.register_view(
|
||||
MerakiView(config, async_see))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MerakiView(HomeAssistantView):
|
||||
"""View to handle Meraki requests."""
|
||||
|
||||
url = URL
|
||||
name = 'api:meraki'
|
||||
|
||||
def __init__(self, config, async_see):
|
||||
"""Initialize Meraki URL endpoints."""
|
||||
self.async_see = async_see
|
||||
self.validator = config[CONF_VALIDATOR]
|
||||
self.secret = config[CONF_SECRET]
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Meraki message received as GET."""
|
||||
return self.validator
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Meraki CMX message received."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||
_LOGGER.debug("Meraki Data from Post: %s", json.dumps(data))
|
||||
if not data.get('secret', False):
|
||||
_LOGGER.error("secret invalid")
|
||||
return self.json_message('No secret', HTTP_UNPROCESSABLE_ENTITY)
|
||||
if data['secret'] != self.secret:
|
||||
_LOGGER.error("Invalid Secret received from Meraki")
|
||||
return self.json_message('Invalid secret',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
elif data['version'] != VERSION:
|
||||
_LOGGER.error("Invalid API version: %s", data['version'])
|
||||
return self.json_message('Invalid version',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
else:
|
||||
_LOGGER.debug('Valid Secret')
|
||||
if data['type'] not in ('DevicesSeen', 'BluetoothDevicesSeen'):
|
||||
_LOGGER.error("Unknown Device %s", data['type'])
|
||||
return self.json_message('Invalid device type',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.debug("Processing %s", data['type'])
|
||||
if len(data["data"]["observations"]) == 0:
|
||||
_LOGGER.debug("No observations found")
|
||||
return
|
||||
self._handle(request.app['hass'], data)
|
||||
|
||||
@callback
|
||||
def _handle(self, hass, data):
|
||||
for i in data["data"]["observations"]:
|
||||
data["data"]["secret"] = "hidden"
|
||||
|
||||
lat = i["location"]["lat"]
|
||||
lng = i["location"]["lng"]
|
||||
try:
|
||||
accuracy = int(float(i["location"]["unc"]))
|
||||
except ValueError:
|
||||
accuracy = 0
|
||||
|
||||
mac = i["clientMac"]
|
||||
_LOGGER.debug("clientMac: %s", mac)
|
||||
|
||||
if lat == "NaN" or lng == "NaN":
|
||||
_LOGGER.debug(
|
||||
"No coordinates received, skipping location for: " + mac
|
||||
)
|
||||
gps_location = None
|
||||
accuracy = None
|
||||
else:
|
||||
gps_location = (lat, lng)
|
||||
|
||||
attrs = {}
|
||||
if i.get('os', False):
|
||||
attrs['os'] = i['os']
|
||||
if i.get('manufacturer', False):
|
||||
attrs['manufacturer'] = i['manufacturer']
|
||||
if i.get('ipv4', False):
|
||||
attrs['ipv4'] = i['ipv4']
|
||||
if i.get('ipv6', False):
|
||||
attrs['ipv6'] = i['ipv6']
|
||||
if i.get('seenTime', False):
|
||||
attrs['seenTime'] = i['seenTime']
|
||||
if i.get('ssid', False):
|
||||
attrs['ssid'] = i['ssid']
|
||||
hass.async_add_job(self.async_see(
|
||||
gps=gps_location,
|
||||
mac=mac,
|
||||
source_type=SOURCE_TYPE_ROUTER,
|
||||
gps_accuracy=accuracy,
|
||||
attributes=attrs
|
||||
))
|
||||
134
homeassistant/components/device_tracker/unifi_direct.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Support for Unifi AP direct access.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.unifi_direct/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
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_PASSWORD, CONF_USERNAME,
|
||||
CONF_PORT)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_SSH_PORT = 22
|
||||
UNIFI_COMMAND = 'mca-dump | tr -d "\n"'
|
||||
UNIFI_SSID_TABLE = "vap_table"
|
||||
UNIFI_CLIENT_TABLE = "sta_table"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Unifi direct scanner."""
|
||||
scanner = UnifiDeviceScanner(config[DOMAIN])
|
||||
if not scanner.connected:
|
||||
return False
|
||||
return scanner
|
||||
|
||||
|
||||
class UnifiDeviceScanner(DeviceScanner):
|
||||
"""This class queries Unifi wireless access point."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.port = config[CONF_PORT]
|
||||
self.ssh = None
|
||||
self.connected = False
|
||||
self.last_results = {}
|
||||
self._connect()
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
result = _response_to_json(self._get_update())
|
||||
if result:
|
||||
self.last_results = result
|
||||
return self.last_results.keys()
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
hostname = next((
|
||||
value.get('hostname') for key, value in self.last_results.items()
|
||||
if key.upper() == device.upper()), None)
|
||||
if hostname is not None:
|
||||
hostname = str(hostname)
|
||||
return hostname
|
||||
|
||||
def _connect(self):
|
||||
"""Connect to the Unifi AP SSH server."""
|
||||
from pexpect import pxssh, exceptions
|
||||
|
||||
self.ssh = pxssh.pxssh()
|
||||
try:
|
||||
self.ssh.login(self.host, self.username,
|
||||
password=self.password, port=self.port)
|
||||
self.connected = True
|
||||
except exceptions.EOF:
|
||||
_LOGGER.error("Connection refused. SSH enabled?")
|
||||
self._disconnect()
|
||||
|
||||
def _disconnect(self):
|
||||
"""Disconnect the current SSH connection."""
|
||||
# pylint: disable=broad-except
|
||||
try:
|
||||
self.ssh.logout()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.ssh = None
|
||||
|
||||
self.connected = False
|
||||
|
||||
def _get_update(self):
|
||||
from pexpect import pxssh
|
||||
|
||||
try:
|
||||
if not self.connected:
|
||||
self._connect()
|
||||
self.ssh.sendline(UNIFI_COMMAND)
|
||||
self.ssh.prompt()
|
||||
return self.ssh.before
|
||||
except pxssh.ExceptionPxssh as err:
|
||||
_LOGGER.error("Unexpected SSH error: %s", str(err))
|
||||
self._disconnect()
|
||||
return None
|
||||
except AssertionError as err:
|
||||
_LOGGER.error("Connection to AP unavailable: %s", str(err))
|
||||
self._disconnect()
|
||||
return None
|
||||
|
||||
|
||||
def _response_to_json(response):
|
||||
try:
|
||||
json_response = json.loads(str(response)[31:-1].replace("\\", ""))
|
||||
_LOGGER.debug(str(json_response))
|
||||
ssid_table = json_response.get(UNIFI_SSID_TABLE)
|
||||
active_clients = {}
|
||||
|
||||
for ssid in ssid_table:
|
||||
client_table = ssid.get(UNIFI_CLIENT_TABLE)
|
||||
for client in client_table:
|
||||
active_clients[client.get("mac")] = client
|
||||
|
||||
return active_clients
|
||||
except ValueError:
|
||||
_LOGGER.error("Failed to decode response from AP.")
|
||||
return {}
|
||||
@@ -35,6 +35,8 @@ SERVICE_AXIS = 'axis'
|
||||
SERVICE_APPLE_TV = 'apple_tv'
|
||||
SERVICE_WINK = 'wink'
|
||||
SERVICE_XIAOMI_GW = 'xiaomi_gw'
|
||||
SERVICE_TELLDUSLIVE = 'tellstick'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
@@ -46,7 +48,8 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_APPLE_TV: ('apple_tv', None),
|
||||
SERVICE_WINK: ('wink', None),
|
||||
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
||||
SERVICE_HUE: ('hue', None),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
|
||||
254
homeassistant/components/dominos.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
Support for Dominos Pizza ordering.
|
||||
|
||||
The Dominos Pizza component ceates a service which can be invoked to order
|
||||
from their menu
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/dominos/.
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import http
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component.
|
||||
DOMAIN = 'dominos'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
ATTR_COUNTRY = 'country_code'
|
||||
ATTR_FIRST_NAME = 'first_name'
|
||||
ATTR_LAST_NAME = 'last_name'
|
||||
ATTR_EMAIL = 'email'
|
||||
ATTR_PHONE = 'phone'
|
||||
ATTR_ADDRESS = 'address'
|
||||
ATTR_ORDERS = 'orders'
|
||||
ATTR_SHOW_MENU = 'show_menu'
|
||||
ATTR_ORDER_ENTITY = 'order_entity_id'
|
||||
ATTR_ORDER_NAME = 'name'
|
||||
ATTR_ORDER_CODES = 'codes'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
MIN_TIME_BETWEEN_STORE_UPDATES = timedelta(minutes=3330)
|
||||
|
||||
REQUIREMENTS = ['pizzapi==0.0.3']
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_ORDERS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ORDER_NAME): cv.string,
|
||||
vol.Required(ATTR_ORDER_CODES): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(ATTR_COUNTRY): cv.string,
|
||||
vol.Required(ATTR_FIRST_NAME): cv.string,
|
||||
vol.Required(ATTR_LAST_NAME): cv.string,
|
||||
vol.Required(ATTR_EMAIL): cv.string,
|
||||
vol.Required(ATTR_PHONE): cv.string,
|
||||
vol.Required(ATTR_ADDRESS): cv.string,
|
||||
vol.Optional(ATTR_SHOW_MENU): cv.boolean,
|
||||
vol.Optional(ATTR_ORDERS, default=[]): vol.All(
|
||||
cv.ensure_list, [_ORDERS_SCHEMA]),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up is called when Home Assistant is loading our component."""
|
||||
dominos = Dominos(hass, config)
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
hass.data[DOMAIN] = {}
|
||||
entities = []
|
||||
conf = config[DOMAIN]
|
||||
|
||||
hass.services.register(DOMAIN, 'order', dominos.handle_order)
|
||||
|
||||
if conf.get(ATTR_SHOW_MENU):
|
||||
hass.http.register_view(DominosProductListView(dominos))
|
||||
|
||||
for order_info in conf.get(ATTR_ORDERS):
|
||||
order = DominosOrder(order_info, dominos)
|
||||
entities.append(order)
|
||||
|
||||
if entities:
|
||||
component.add_entities(entities)
|
||||
|
||||
# Return boolean to indicate that initialization was successfully.
|
||||
return True
|
||||
|
||||
|
||||
class Dominos():
|
||||
"""Main Dominos service."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Set up main service."""
|
||||
conf = config[DOMAIN]
|
||||
from pizzapi import Address, Customer
|
||||
from pizzapi.address import StoreException
|
||||
self.hass = hass
|
||||
self.customer = Customer(
|
||||
conf.get(ATTR_FIRST_NAME),
|
||||
conf.get(ATTR_LAST_NAME),
|
||||
conf.get(ATTR_EMAIL),
|
||||
conf.get(ATTR_PHONE),
|
||||
conf.get(ATTR_ADDRESS))
|
||||
self.address = Address(
|
||||
*self.customer.address.split(','),
|
||||
country=conf.get(ATTR_COUNTRY))
|
||||
self.country = conf.get(ATTR_COUNTRY)
|
||||
try:
|
||||
self.closest_store = self.address.closest_store()
|
||||
except StoreException:
|
||||
self.closest_store = None
|
||||
|
||||
def handle_order(self, call):
|
||||
"""Handle ordering pizza."""
|
||||
entity_ids = call.data.get(ATTR_ORDER_ENTITY, None)
|
||||
|
||||
target_orders = [order for order in self.hass.data[DOMAIN]['entities']
|
||||
if order.entity_id in entity_ids]
|
||||
|
||||
for order in target_orders:
|
||||
order.place()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_STORE_UPDATES)
|
||||
def update_closest_store(self):
|
||||
"""Update the shared closest store (if open)."""
|
||||
from pizzapi.address import StoreException
|
||||
try:
|
||||
self.closest_store = self.address.closest_store()
|
||||
return True
|
||||
except StoreException:
|
||||
self.closest_store = None
|
||||
return False
|
||||
|
||||
def get_menu(self):
|
||||
"""Return the products from the closest stores menu."""
|
||||
self.update_closest_store()
|
||||
if self.closest_store is None:
|
||||
_LOGGER.warning('Cannot get menu. Store may be closed')
|
||||
return []
|
||||
else:
|
||||
menu = self.closest_store.get_menu()
|
||||
product_entries = []
|
||||
|
||||
for product in menu.products:
|
||||
item = {}
|
||||
if isinstance(product.menu_data['Variants'], list):
|
||||
variants = ', '.join(product.menu_data['Variants'])
|
||||
else:
|
||||
variants = product.menu_data['Variants']
|
||||
item['name'] = product.name
|
||||
item['variants'] = variants
|
||||
product_entries.append(item)
|
||||
|
||||
return product_entries
|
||||
|
||||
|
||||
class DominosProductListView(http.HomeAssistantView):
|
||||
"""View to retrieve product list content."""
|
||||
|
||||
url = '/api/dominos'
|
||||
name = "api:dominos"
|
||||
|
||||
def __init__(self, dominos):
|
||||
"""Initialize suite view."""
|
||||
self.dominos = dominos
|
||||
|
||||
@callback
|
||||
def get(self, request):
|
||||
"""Retrieve if API is running."""
|
||||
return self.json(self.dominos.get_menu())
|
||||
|
||||
|
||||
class DominosOrder(Entity):
|
||||
"""Represents a Dominos order entity."""
|
||||
|
||||
def __init__(self, order_info, dominos):
|
||||
"""Set up the entity."""
|
||||
self._name = order_info['name']
|
||||
self._product_codes = order_info['codes']
|
||||
self._orderable = False
|
||||
self.dominos = dominos
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the orders name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def product_codes(self):
|
||||
"""Return the orders product codes."""
|
||||
return self._product_codes
|
||||
|
||||
@property
|
||||
def orderable(self):
|
||||
"""Return the true if orderable."""
|
||||
return self._orderable
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state either closed, orderable or unorderable."""
|
||||
if self.dominos.closest_store is None:
|
||||
return 'closed'
|
||||
else:
|
||||
return 'orderable' if self._orderable else 'unorderable'
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update the order state and refreshes the store."""
|
||||
from pizzapi.address import StoreException
|
||||
try:
|
||||
self.dominos.update_closest_store()
|
||||
except StoreException:
|
||||
self._orderable = False
|
||||
return
|
||||
|
||||
try:
|
||||
order = self.order()
|
||||
order.pay_with()
|
||||
self._orderable = True
|
||||
except StoreException:
|
||||
self._orderable = False
|
||||
|
||||
def order(self):
|
||||
"""Create the order object."""
|
||||
from pizzapi import Order
|
||||
from pizzapi.address import StoreException
|
||||
|
||||
if self.dominos.closest_store is None:
|
||||
raise StoreException
|
||||
|
||||
order = Order(
|
||||
self.dominos.closest_store,
|
||||
self.dominos.customer,
|
||||
self.dominos.address,
|
||||
self.dominos.country)
|
||||
|
||||
for code in self._product_codes:
|
||||
order.add_item(code)
|
||||
|
||||
return order
|
||||
|
||||
def place(self):
|
||||
"""Place the order."""
|
||||
from pizzapi.address import StoreException
|
||||
try:
|
||||
order = self.order()
|
||||
order.place()
|
||||
except StoreException:
|
||||
self._orderable = False
|
||||
_LOGGER.warning(
|
||||
'Attempted to order Dominos - Order invalid or store closed')
|
||||
@@ -6,7 +6,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['DoorBirdPy==0.0.4']
|
||||
REQUIREMENTS = ['DoorBirdPy==0.1.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
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.10']
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.14']
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -81,6 +82,7 @@ def setup_ecobee(hass, network, config):
|
||||
hass, 'climate', DOMAIN, {'hold_temp': hold_temp}, config)
|
||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'weather', DOMAIN, {}, config)
|
||||
|
||||
|
||||
class EcobeeData(object):
|
||||
@@ -110,12 +112,10 @@ def setup(hass, config):
|
||||
if 'ecobee' in _CONFIGURING:
|
||||
return
|
||||
|
||||
from pyecobee import config_from_file
|
||||
|
||||
# Create ecobee.conf if it doesn't exist
|
||||
if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)):
|
||||
jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)}
|
||||
config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
|
||||
save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
|
||||
|
||||
NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE))
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/emulated_hue/
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -16,8 +15,10 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.components.http import REQUIREMENTS # NOQA
|
||||
from homeassistant.components.http import HomeAssistantWSGI
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
from .hue_api import (
|
||||
HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
|
||||
HueOneLightChangeView)
|
||||
@@ -136,7 +137,7 @@ class Config(object):
|
||||
self.host_ip_addr = conf.get(CONF_HOST_IP)
|
||||
if self.host_ip_addr is None:
|
||||
self.host_ip_addr = util.get_local_ip()
|
||||
_LOGGER.warning(
|
||||
_LOGGER.info(
|
||||
"Listen IP address not specified, auto-detected address is %s",
|
||||
self.host_ip_addr)
|
||||
|
||||
@@ -144,7 +145,7 @@ class Config(object):
|
||||
self.listen_port = conf.get(CONF_LISTEN_PORT)
|
||||
if not isinstance(self.listen_port, int):
|
||||
self.listen_port = DEFAULT_LISTEN_PORT
|
||||
_LOGGER.warning(
|
||||
_LOGGER.info(
|
||||
"Listen port not specified, defaulting to %s",
|
||||
self.listen_port)
|
||||
|
||||
@@ -187,7 +188,7 @@ class Config(object):
|
||||
return entity_id
|
||||
|
||||
if self.numbers is None:
|
||||
self.numbers = self._load_numbers_json()
|
||||
self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
|
||||
|
||||
# Google Home
|
||||
for number, ent_id in self.numbers.items():
|
||||
@@ -198,7 +199,7 @@ class Config(object):
|
||||
if self.numbers:
|
||||
number = str(max(int(k) for k in self.numbers) + 1)
|
||||
self.numbers[number] = entity_id
|
||||
self._save_numbers_json()
|
||||
save_json(self.hass.config.path(NUMBERS_FILE), self.numbers)
|
||||
return number
|
||||
|
||||
def number_to_entity_id(self, number):
|
||||
@@ -207,7 +208,7 @@ class Config(object):
|
||||
return number
|
||||
|
||||
if self.numbers is None:
|
||||
self.numbers = self._load_numbers_json()
|
||||
self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
|
||||
|
||||
# Google Home
|
||||
assert isinstance(number, str)
|
||||
@@ -244,25 +245,11 @@ class Config(object):
|
||||
|
||||
return is_default_exposed or expose
|
||||
|
||||
def _load_numbers_json(self):
|
||||
"""Set up helper method to load numbers json."""
|
||||
try:
|
||||
with open(self.hass.config.path(NUMBERS_FILE),
|
||||
encoding='utf-8') as fil:
|
||||
return json.loads(fil.read())
|
||||
except (OSError, ValueError) as err:
|
||||
# OSError if file not found or unaccessible/no permissions
|
||||
# ValueError if could not parse JSON
|
||||
if not isinstance(err, FileNotFoundError):
|
||||
_LOGGER.warning("Failed to open %s: %s", NUMBERS_FILE, err)
|
||||
return {}
|
||||
|
||||
def _save_numbers_json(self):
|
||||
"""Set up helper method to save numbers json."""
|
||||
try:
|
||||
with open(self.hass.config.path(NUMBERS_FILE), 'w',
|
||||
encoding='utf-8') as fil:
|
||||
fil.write(json.dumps(self.numbers))
|
||||
except OSError as err:
|
||||
# OSError if file write permissions
|
||||
_LOGGER.warning("Failed to write %s: %s", NUMBERS_FILE, err)
|
||||
def _load_json(filename):
|
||||
"""Wrapper, because we actually want to handle invalid json."""
|
||||
try:
|
||||
return load_json(filename)
|
||||
except HomeAssistantError:
|
||||
pass
|
||||
return {}
|
||||
|
||||
@@ -4,9 +4,7 @@ Support for Insteon fans via local hub control.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.insteon_local/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
@@ -14,6 +12,7 @@ from homeassistant.components.fan import (
|
||||
SUPPORT_SET_SPEED, FanEntity)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -33,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Insteon local fan platform."""
|
||||
insteonhub = hass.data['insteon_local']
|
||||
|
||||
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
|
||||
conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF))
|
||||
if conf_fans:
|
||||
for device_id in conf_fans:
|
||||
setup_fan(device_id, conf_fans[device_id], insteonhub, hass,
|
||||
@@ -88,44 +87,16 @@ def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
|
||||
configurator.request_done(request_id)
|
||||
_LOGGER.info("Device configuration done!")
|
||||
|
||||
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
|
||||
conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF))
|
||||
if device_id not in conf_fans:
|
||||
conf_fans[device_id] = name
|
||||
|
||||
if not config_from_file(
|
||||
hass.config.path(INSTEON_LOCAL_FANS_CONF),
|
||||
conf_fans):
|
||||
_LOGGER.error("Failed to save configuration file")
|
||||
save_json(hass.config.path(INSTEON_LOCAL_FANS_CONF), conf_fans)
|
||||
|
||||
device = insteonhub.fan(device_id)
|
||||
add_devices_callback([InsteonLocalFanDevice(device, name)])
|
||||
|
||||
|
||||
def config_from_file(filename, config=None):
|
||||
"""Small configuration file management function."""
|
||||
if config:
|
||||
# We're writing configuration
|
||||
try:
|
||||
with open(filename, 'w') as fdesc:
|
||||
fdesc.write(json.dumps(config))
|
||||
except IOError as error:
|
||||
_LOGGER.error('Saving config file failed: %s', error)
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
# We're reading config
|
||||
if os.path.isfile(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fdesc:
|
||||
return json.loads(fdesc.read())
|
||||
except IOError as error:
|
||||
_LOGGER.error("Reading configuration file failed: %s", error)
|
||||
# This won't work yet
|
||||
return False
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
class InsteonLocalFanDevice(FanEntity):
|
||||
"""An abstract Class for an Insteon node."""
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.1']
|
||||
REQUIREMENTS = ['python-miio==0.3.2']
|
||||
|
||||
ATTR_TEMPERATURE = 'temperature'
|
||||
ATTR_HUMIDITY = 'humidity'
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20171118.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20171216.0', 'user-agents==1.1.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
@@ -32,9 +32,10 @@ URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
|
||||
CONF_THEMES = 'themes'
|
||||
CONF_EXTRA_HTML_URL = 'extra_html_url'
|
||||
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
|
||||
CONF_FRONTEND_REPO = 'development_repo'
|
||||
CONF_JS_VERSION = 'javascript_version'
|
||||
JS_DEFAULT_OPTION = 'es5'
|
||||
JS_DEFAULT_OPTION = 'auto'
|
||||
JS_OPTIONS = ['es5', 'latest', 'auto']
|
||||
|
||||
DEFAULT_THEME_COLOR = '#03A9F4'
|
||||
@@ -63,6 +64,7 @@ DATA_FINALIZE_PANEL = 'frontend_finalize_panel'
|
||||
DATA_PANELS = 'frontend_panels'
|
||||
DATA_JS_VERSION = 'frontend_js_version'
|
||||
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
|
||||
DATA_EXTRA_HTML_URL_ES5 = 'frontend_extra_html_url_es5'
|
||||
DATA_THEMES = 'frontend_themes'
|
||||
DATA_DEFAULT_THEME = 'frontend_default_theme'
|
||||
DEFAULT_THEME = 'default'
|
||||
@@ -79,6 +81,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
vol.Optional(CONF_EXTRA_HTML_URL):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_EXTRA_HTML_URL_ES5):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
|
||||
vol.In(JS_OPTIONS)
|
||||
}),
|
||||
@@ -269,11 +273,12 @@ def async_register_panel(hass, component_name, path, md5=None,
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def add_extra_html_url(hass, url):
|
||||
def add_extra_html_url(hass, url, es5=False):
|
||||
"""Register extra html url to load."""
|
||||
url_set = hass.data.get(DATA_EXTRA_HTML_URL)
|
||||
key = DATA_EXTRA_HTML_URL_ES5 if es5 else DATA_EXTRA_HTML_URL
|
||||
url_set = hass.data.get(key)
|
||||
if url_set is None:
|
||||
url_set = hass.data[DATA_EXTRA_HTML_URL] = set()
|
||||
url_set = hass.data[key] = set()
|
||||
url_set.add(url)
|
||||
|
||||
|
||||
@@ -294,11 +299,16 @@ def async_setup(hass, config):
|
||||
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
|
||||
|
||||
if is_dev:
|
||||
hass.http.register_static_path(
|
||||
"/home-assistant-polymer", repo_path, False)
|
||||
for subpath in ["src", "build-translations", "build-temp", "build",
|
||||
"hass_frontend", "bower_components", "panels"]:
|
||||
hass.http.register_static_path(
|
||||
"/home-assistant-polymer/{}".format(subpath),
|
||||
os.path.join(repo_path, subpath),
|
||||
False)
|
||||
|
||||
hass.http.register_static_path(
|
||||
"/static/translations",
|
||||
os.path.join(repo_path, "build-translations"), False)
|
||||
os.path.join(repo_path, "build-translations/output"), False)
|
||||
sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js")
|
||||
sw_path_latest = os.path.join(repo_path, "build/service_worker.js")
|
||||
static_path = os.path.join(repo_path, 'hass_frontend')
|
||||
@@ -358,9 +368,13 @@ def async_setup(hass, config):
|
||||
|
||||
if DATA_EXTRA_HTML_URL not in hass.data:
|
||||
hass.data[DATA_EXTRA_HTML_URL] = set()
|
||||
if DATA_EXTRA_HTML_URL_ES5 not in hass.data:
|
||||
hass.data[DATA_EXTRA_HTML_URL_ES5] = set()
|
||||
|
||||
for url in conf.get(CONF_EXTRA_HTML_URL, []):
|
||||
add_extra_html_url(hass, url)
|
||||
add_extra_html_url(hass, url, False)
|
||||
for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []):
|
||||
add_extra_html_url(hass, url, True)
|
||||
|
||||
yield from async_setup_themes(hass, conf.get(CONF_THEMES))
|
||||
|
||||
@@ -467,7 +481,8 @@ class IndexView(HomeAssistantView):
|
||||
def get(self, request, extra=None):
|
||||
"""Serve the index view."""
|
||||
hass = request.app['hass']
|
||||
latest = _is_latest(self.js_option, request)
|
||||
latest = self.repo_path is not None or \
|
||||
_is_latest(self.js_option, request)
|
||||
|
||||
if request.path == '/':
|
||||
panel = 'states'
|
||||
@@ -481,21 +496,21 @@ class IndexView(HomeAssistantView):
|
||||
else:
|
||||
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
|
||||
|
||||
no_auth = 'true'
|
||||
no_auth = '1'
|
||||
if hass.config.api.api_password and not is_trusted_ip(request):
|
||||
# do not try to auto connect on load
|
||||
no_auth = 'false'
|
||||
no_auth = '0'
|
||||
|
||||
template = yield from hass.async_add_job(self.get_template, latest)
|
||||
|
||||
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
|
||||
|
||||
resp = template.render(
|
||||
no_auth=no_auth,
|
||||
panel_url=panel_url,
|
||||
panels=hass.data[DATA_PANELS],
|
||||
dev_mode=self.repo_path is not None,
|
||||
theme_color=MANIFEST_JSON['theme_color'],
|
||||
extra_urls=hass.data[DATA_EXTRA_HTML_URL],
|
||||
latest=latest,
|
||||
extra_urls=hass.data[extra_key],
|
||||
)
|
||||
|
||||
return web.Response(text=resp, content_type='text/html')
|
||||
@@ -547,10 +562,37 @@ def _is_latest(js_option, request):
|
||||
"""
|
||||
if request is None:
|
||||
return js_option == 'latest'
|
||||
latest_in_query = 'latest' in request.query or (
|
||||
request.headers.get('Referer') and
|
||||
'latest' in urlparse(request.headers['Referer']).query)
|
||||
es5_in_query = 'es5' in request.query or (
|
||||
request.headers.get('Referer') and
|
||||
'es5' in urlparse(request.headers['Referer']).query)
|
||||
return latest_in_query or (not es5_in_query and js_option == 'latest')
|
||||
|
||||
# latest in query
|
||||
if 'latest' in request.query or (
|
||||
request.headers.get('Referer') and
|
||||
'latest' in urlparse(request.headers['Referer']).query):
|
||||
return True
|
||||
|
||||
# es5 in query
|
||||
if 'es5' in request.query or (
|
||||
request.headers.get('Referer') and
|
||||
'es5' in urlparse(request.headers['Referer']).query):
|
||||
return False
|
||||
|
||||
# non-auto option in config
|
||||
if js_option != 'auto':
|
||||
return js_option == 'latest'
|
||||
|
||||
from user_agents import parse
|
||||
useragent = parse(request.headers.get('User-Agent'))
|
||||
|
||||
# on iOS every browser is a Safari which we support from version 11.
|
||||
if useragent.os.family == 'iOS':
|
||||
# Was >= 10, temp setting it to 12 to work around issue #11387
|
||||
return useragent.os.version[0] >= 12
|
||||
|
||||
family_min_version = {
|
||||
'Chrome': 50, # Probably can reduce this
|
||||
'Firefox': 43, # Array.protopype.includes added in 43
|
||||
'Opera': 40, # Probably can reduce this
|
||||
'Edge': 14, # Array.protopype.includes added in 14
|
||||
'Safari': 10, # many features not supported by 9
|
||||
}
|
||||
version = family_min_version.get(useragent.browser.family)
|
||||
return version and useragent.browser.version[0] >= version
|
||||
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
@@ -126,21 +126,23 @@ class GoogleAssistantView(HomeAssistantView):
|
||||
commands = []
|
||||
for command in requested_commands:
|
||||
ent_ids = [ent.get('id') for ent in command.get('devices', [])]
|
||||
execution = command.get('execution')[0]
|
||||
for eid in ent_ids:
|
||||
success = False
|
||||
domain = eid.split('.')[0]
|
||||
(service, service_data) = determine_service(
|
||||
eid, execution.get('command'), execution.get('params'),
|
||||
hass.config.units)
|
||||
success = yield from hass.services.async_call(
|
||||
domain, service, service_data, blocking=True)
|
||||
result = {"ids": [eid], "states": {}}
|
||||
if success:
|
||||
result['status'] = 'SUCCESS'
|
||||
else:
|
||||
result['status'] = 'ERROR'
|
||||
commands.append(result)
|
||||
for execution in command.get('execution'):
|
||||
for eid in ent_ids:
|
||||
success = False
|
||||
domain = eid.split('.')[0]
|
||||
(service, service_data) = determine_service(
|
||||
eid, execution.get('command'), execution.get('params'),
|
||||
hass.config.units)
|
||||
if domain == "group":
|
||||
domain = "homeassistant"
|
||||
success = yield from hass.services.async_call(
|
||||
domain, service, service_data, blocking=True)
|
||||
result = {"ids": [eid], "states": {}}
|
||||
if success:
|
||||
result['status'] = 'SUCCESS'
|
||||
else:
|
||||
result['status'] = 'ERROR'
|
||||
commands.append(result)
|
||||
|
||||
return self.json(
|
||||
_make_actions_response(request_id, {'commands': commands}))
|
||||
|
||||