Compare commits
365 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4e2332ce0 | |||
| e4685de459 | |||
| 73547c8c4b | |||
| 0b327cd4d9 | |||
| dfe9af7110 | |||
| 016e8f833d | |||
| 574df0f420 | |||
| f18f181962 | |||
| 4754455295 | |||
| 8c1317f278 | |||
| c9a88322d6 | |||
| 46b58db2ff | |||
| 78cbfa3f96 | |||
| dba5c74c8f | |||
| a94a5ac9b5 | |||
| 4239a2b844 | |||
| 408b68bfac | |||
| fe317b806f | |||
| 09cbf68637 | |||
| fb94aaa5a1 | |||
| b09b13f552 | |||
| 2d4df42a65 | |||
| fccc7e69d0 | |||
| f1e5d32ef5 | |||
| 1c24018fbb | |||
| f37038921f | |||
| f030ff67ad | |||
| 3cfec2b5e2 | |||
| 58b3dd7cc0 | |||
| 5e16dc6307 | |||
| 8b059e9aad | |||
| 12f1be9b1c | |||
| 519f400175 | |||
| a94571fd10 | |||
| cb3a78b330 | |||
| 1bc6366051 | |||
| 5d339fb141 | |||
| 0c68f381b0 | |||
| 3e40b24293 | |||
| 4b828d225e | |||
| 201294e481 | |||
| 350d23f7eb | |||
| b8beae9c6c | |||
| 46f3337b07 | |||
| b2354f45be | |||
| e4e13b59cf | |||
| 8c694eb279 | |||
| 9d085a023c | |||
| c06313a897 | |||
| ed490b1c11 | |||
| 70fc94003d | |||
| 114fae76e1 | |||
| 4f0064b00e | |||
| dc53c21548 | |||
| b9b41d3855 | |||
| 17a8dd3f70 | |||
| 03d6a7c42a | |||
| d2d393feb5 | |||
| e49651cdeb | |||
| a60e845203 | |||
| f23eb9336f | |||
| 0bf8bb62ad | |||
| 5085cdb0f7 | |||
| 8358f938b5 | |||
| be7401f4a2 | |||
| 760117167d | |||
| 2fd83b439c | |||
| e523c3d196 | |||
| d9e73d1d71 | |||
| 5d3e93b4ef | |||
| 85782f9c29 | |||
| 2cf49f3de6 | |||
| a072047d9d | |||
| 74b0e4cb45 | |||
| 694983379f | |||
| 5900d8a2c1 | |||
| 287a7e2720 | |||
| 8592ba3cb9 | |||
| c93b63963b | |||
| 194402f7e7 | |||
| d58548dd1c | |||
| f2a12b7ac2 | |||
| 625319846c | |||
| c3ea04f2dd | |||
| 4a5a1e1e96 | |||
| 41aff96375 | |||
| 0219df17f5 | |||
| b586e80977 | |||
| 73d93e526e | |||
| 148ea82513 | |||
| abb8bcb6d9 | |||
| e455daa61d | |||
| b6b0bad0c7 | |||
| d5ab292ff3 | |||
| 9f8acbec95 | |||
| d55ed7a3a2 | |||
| fcbfcf0aa7 | |||
| 183936375f | |||
| 1f1ac02457 | |||
| 646eaccd4d | |||
| c189c05676 | |||
| 9683e3e3ad | |||
| a61e08680a | |||
| 9da2d6edd0 | |||
| 1f38e9fa57 | |||
| 996d7cf1cd | |||
| c36d30f4fe | |||
| 8f3e12c9b8 | |||
| 56fdc2a625 | |||
| e18825ba20 | |||
| 7ab7edd81c | |||
| 16ff68ca84 | |||
| b8504f8fc8 | |||
| 185bd6c28a | |||
| 33a51623f8 | |||
| 4198c42736 | |||
| 3e24a35c1e | |||
| 651f3ab55c | |||
| 756f23f0b4 | |||
| ef0e018cbb | |||
| 9cf2ad0b55 | |||
| 892bbdc2dd | |||
| bb03960ba5 | |||
| 15ed8c6332 | |||
| 412b5350ce | |||
| d5f8aa52c4 | |||
| b650b2b0db | |||
| 7e50ccd32a | |||
| 521080d1b0 | |||
| d76cf092c3 | |||
| dfb92fa836 | |||
| 5cb8ce71ef | |||
| 099e983ca0 | |||
| 39514be1f9 | |||
| 234f4449b0 | |||
| a89d036e26 | |||
| 9ea030f42e | |||
| 7ac8425099 | |||
| c000e74d0a | |||
| 6632747543 | |||
| a7266ae6cf | |||
| 4031f70665 | |||
| 2b59409e52 | |||
| b41c795d34 | |||
| db56ed400d | |||
| c603ffbe26 | |||
| 68028afb98 | |||
| 733120c577 | |||
| 01435f7f42 | |||
| 77c91c8a5e | |||
| a321c2f0d8 | |||
| 807daf8f5d | |||
| 08bacd8e31 | |||
| f79d762e66 | |||
| 30aa67b789 | |||
| 883e45c476 | |||
| dd551cf056 | |||
| 47a3adb6b3 | |||
| 7d86fb8c72 | |||
| e87467cb20 | |||
| abd1213cc4 | |||
| f3d838c448 | |||
| 574e3231d0 | |||
| 48ffdea31f | |||
| 8e14da7454 | |||
| 6e80581b30 | |||
| 9780ea5c52 | |||
| c4d817146f | |||
| f3caca6a06 | |||
| d6c586bf42 | |||
| 3b5142eb85 | |||
| 4b8bc90d16 | |||
| a084232cf5 | |||
| 00e298206e | |||
| 559f63bfc3 | |||
| 6694b0470e | |||
| da6c09640c | |||
| 2505792ef3 | |||
| 4c45e92116 | |||
| 041c92699a | |||
| d761b000a5 | |||
| cae10cfe26 | |||
| ea4f49f0a0 | |||
| bbfd86dec3 | |||
| 0c0feda834 | |||
| b3d67a7ed9 | |||
| 986873834a | |||
| 36921748ed | |||
| b628fb088b | |||
| 4a5cc5ad3d | |||
| a1488b46f6 | |||
| ac4e54c6ff | |||
| bac8ffdd52 | |||
| d3a012a536 | |||
| e00a469828 | |||
| 1b9d867d60 | |||
| 8d0009b894 | |||
| ad2dea939b | |||
| 2ecbcac2b1 | |||
| 3d31d26b6c | |||
| 0065dc0cd7 | |||
| e4c5f356e2 | |||
| 9631179126 | |||
| de51cfbc07 | |||
| de4c63b437 | |||
| d5912f41fb | |||
| 03b2c48d45 | |||
| 65b1a731ca | |||
| 7625aae373 | |||
| 8251039ca4 | |||
| 3418d03e69 | |||
| f1caf3f2b5 | |||
| 3dea4be2cc | |||
| 8f9fea37b2 | |||
| 380993f2ca | |||
| 5fd93e8d80 | |||
| bc9d2586c6 | |||
| ad7683470a | |||
| 329474d3e3 | |||
| b7430d939d | |||
| e5af126fae | |||
| 9aff839925 | |||
| 287f9c9bda | |||
| a6673f6741 | |||
| 784cf0c4bd | |||
| 5966c46a67 | |||
| b9992a9914 | |||
| edf812c0ea | |||
| a310599a03 | |||
| 4c625d09aa | |||
| 0335f88e61 | |||
| 769bc37150 | |||
| 138205a019 | |||
| e891f1a260 | |||
| eb1871dc5b | |||
| f75b0a99d9 | |||
| 81ebdadcec | |||
| de5bd26050 | |||
| 54248863b3 | |||
| 43c395232a | |||
| 68835c4b4b | |||
| be68fe0d85 | |||
| d31f6bc3f0 | |||
| ae1b69430e | |||
| a998846961 | |||
| 9ac39df33f | |||
| fa2ce366de | |||
| 35603268ca | |||
| d6ad4bc22b | |||
| 87fe83dcb9 | |||
| 256062fd99 | |||
| 8a99ce78c2 | |||
| 9a87e5e336 | |||
| da8994e4b5 | |||
| b34101b277 | |||
| 11c07440fe | |||
| 2c43d6718b | |||
| de4cc5034e | |||
| c89a77dc74 | |||
| 91e36f380b | |||
| 169f054c6c | |||
| 9184773f8f | |||
| e19a092934 | |||
| 1c706834e0 | |||
| 04d31e4ef4 | |||
| 2b7d1fe20d | |||
| a90049568e | |||
| 534f56a3e2 | |||
| 81928b1a6b | |||
| 325220e009 | |||
| aca375c312 | |||
| 4076ccf639 | |||
| d7452f9d5d | |||
| c23ad3e285 | |||
| 0a6f496425 | |||
| 07a92e8ac3 | |||
| d1b08824e8 | |||
| c693db49b3 | |||
| b58976bc36 | |||
| 70a79efb77 | |||
| 982a0bc195 | |||
| 9ad592e606 | |||
| 6f840de1d2 | |||
| d029861c93 | |||
| c6fa07d059 | |||
| 782838af56 | |||
| 7724cb9eb4 | |||
| b78e98702a | |||
| 727b756054 | |||
| 4791b5679e | |||
| 79bff0fc57 | |||
| 1e8cf8c1b7 | |||
| 26a118e75d | |||
| e7f9fdca67 | |||
| 0c7c85dbfe | |||
| 2c01a67446 | |||
| 68def21615 | |||
| 7528da455c | |||
| 8da85d7a91 | |||
| ac5647a30e | |||
| cb3ab1e873 | |||
| afc527ea55 | |||
| 165362da0c | |||
| 1697a8c774 | |||
| de2eed3c9f | |||
| ca646c08c2 | |||
| e4f4e91096 | |||
| 812dc99073 | |||
| 898cf1b352 | |||
| bba75bf6c3 | |||
| efbc378226 | |||
| 8ba952ee0e | |||
| db3bfad0b5 | |||
| 2b1416c514 | |||
| 8189ec2c8d | |||
| 7f6fb95afd | |||
| 609d7ebea5 | |||
| b9154158e8 | |||
| b43bf62347 | |||
| c6b6ab1b79 | |||
| 07148fc580 | |||
| bc600b8f32 | |||
| dd4611064f | |||
| 24f1bff7f1 | |||
| 6959407dfd | |||
| 14b6f9d927 | |||
| d7e3fa22eb | |||
| a9ef8d8568 | |||
| a69c575dab | |||
| 240cb9b8f0 | |||
| ac063f8e61 | |||
| c028e1fc6f | |||
| 44681ebd55 | |||
| c90cc77c41 | |||
| 71aa1a2f3c | |||
| 0cfa5e5f67 | |||
| f0ec51711c | |||
| d6ca930427 | |||
| 7bce8bc33f | |||
| 36785296ce | |||
| 838b09bb8f | |||
| fa4b253871 | |||
| 360a650370 | |||
| 26abe83be5 | |||
| dba78b02da | |||
| 515c4773f3 | |||
| 05a3b610ff | |||
| 8c7a1b4b05 | |||
| 58c0990508 | |||
| 11396a221e | |||
| ab826eef0d | |||
| d20b4c17a2 | |||
| 1b77b2c3a3 | |||
| f5df5615be | |||
| cc99d266b7 | |||
| aed1348411 | |||
| f6bc63092c | |||
| d48ed41122 | |||
| cce3e284d7 | |||
| ac9151af54 | |||
| f341974b8b | |||
| 78313c793c | |||
| 3f4d30c8da | |||
| 9bbe7be684 | |||
| 1b46ed5045 |
+26
-7
@@ -16,6 +16,9 @@ omit =
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
homeassistant/components/dweet.py
|
||||
homeassistant/components/*/dweet.py
|
||||
|
||||
@@ -46,6 +49,9 @@ omit =
|
||||
homeassistant/components/qwikswitch.py
|
||||
homeassistant/components/*/qwikswitch.py
|
||||
|
||||
homeassistant/components/rfxtrx.py
|
||||
homeassistant/components/*/rfxtrx.py
|
||||
|
||||
homeassistant/components/rpi_gpio.py
|
||||
homeassistant/components/*/rpi_gpio.py
|
||||
|
||||
@@ -77,7 +83,7 @@ omit =
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zwave.py
|
||||
homeassistant/components/zwave/*
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
@@ -93,19 +99,18 @@ omit =
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/switch/knx.py
|
||||
homeassistant/components/binary_sensor/knx.py
|
||||
homeassistant/components/thermostat/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/ffmpeg.py
|
||||
homeassistant/components/*/ffmpeg.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/ffmpeg.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
@@ -136,6 +141,7 @@ omit =
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/device_tracker/volvooncall.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
@@ -147,6 +153,7 @@ omit =
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/hue.py
|
||||
@@ -188,6 +195,7 @@ omit =
|
||||
homeassistant/components/notify/group.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nma.py
|
||||
@@ -196,6 +204,7 @@ omit =
|
||||
homeassistant/components/notify/pushover.py
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/sendgrid.py
|
||||
homeassistant/components/notify/simplepush.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
@@ -203,20 +212,24 @@ omit =
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/openalpr.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
@@ -224,10 +237,12 @@ omit =
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/mhz19.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
@@ -247,16 +262,20 @@ omit =
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yahoo_finance.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||
|
||||
If code communicates with devices, web services, or a:
|
||||
If the code communicates with devices, web services, or third-party tools:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
|
||||
+4
-1
@@ -99,4 +99,7 @@ virtualization/vagrant/config
|
||||
.vscode
|
||||
|
||||
# Built docs
|
||||
docs/build
|
||||
docs/build
|
||||
|
||||
# Windows Explorer
|
||||
desktop.ini
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
FROM python:3.4
|
||||
FROM python:3.5
|
||||
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
|
||||
VOLUME /config
|
||||
@@ -10,7 +10,7 @@ RUN pip3 install --no-cache-dir colorlog cython
|
||||
|
||||
# For the nmap tracker, bluetooth tracker, Z-Wave
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev && \
|
||||
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY script/build_python_openzwave script/build_python_openzwave
|
||||
@@ -21,7 +21,7 @@ RUN script/build_python_openzwave && \
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
# certifi breaks Debian based installs
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
|
||||
pip3 install mysqlclient psycopg2
|
||||
pip3 install mysqlclient psycopg2 uvloop
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
+5
-5
@@ -340,8 +340,8 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Home-Assistant.tex', 'Home-Assistant Documentation',
|
||||
'Home-Assistant Team', 'manual'),
|
||||
(master_doc, 'home-assistant.tex', 'Home Assistant Documentation',
|
||||
'Home Assistant Team', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -382,7 +382,7 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'home-assistant', 'Home-Assistant Documentation',
|
||||
(master_doc, 'home-assistant', 'Home Assistant Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
@@ -397,8 +397,8 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Home-Assistant', 'Home-Assistant Documentation',
|
||||
author, 'Home-Assistant', 'One line description of project.',
|
||||
(master_doc, 'Home-Assistant', 'Home Assistant Documentation',
|
||||
author, 'Home Assistant', 'Open-source home automation platform.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
+55
-10
@@ -16,16 +16,57 @@ from homeassistant.const import (
|
||||
REQUIRED_PYTHON_VER,
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
|
||||
def monkey_patch_asyncio():
|
||||
"""Replace weakref.WeakSet to address Python 3 bug.
|
||||
|
||||
Under heavy threading operations that schedule calls into
|
||||
the asyncio event loop, Task objects are created. Due to
|
||||
a bug in Python, GC may have an issue when switching between
|
||||
the threads and objects with __del__ (which various components
|
||||
in HASS have).
|
||||
|
||||
This monkey-patch removes the weakref.Weakset, and replaces it
|
||||
with an object that ignores the only call utilizing it (the
|
||||
Task.__init__ which calls _all_tasks.add(self)). It also removes
|
||||
the __del__ which could trigger the future objects __del__ at
|
||||
unpredictable times.
|
||||
|
||||
The side-effect of this manipulation of the Task is that
|
||||
Task.all_tasks() is no longer accurate, and there will be no
|
||||
warning emitted if a Task is GC'd while in use.
|
||||
|
||||
On Python 3.6, after the bug is fixed, this monkey-patch can be
|
||||
disabled.
|
||||
|
||||
See https://bugs.python.org/issue26617 for details of the Python
|
||||
bug.
|
||||
"""
|
||||
# pylint: disable=no-self-use, too-few-public-methods, protected-access
|
||||
# pylint: disable=bare-except
|
||||
import asyncio.tasks
|
||||
|
||||
class IgnoreCalls:
|
||||
"""Ignore add calls."""
|
||||
|
||||
def add(self, other):
|
||||
"""No-op add."""
|
||||
return
|
||||
|
||||
asyncio.tasks.Task._all_tasks = IgnoreCalls()
|
||||
try:
|
||||
del asyncio.tasks.Task.__del__
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate we're running the right Python version."""
|
||||
major, minor = sys.version_info[:2]
|
||||
req_major, req_minor = REQUIRED_PYTHON_VER
|
||||
|
||||
if major < req_major or (major == req_major and minor < req_minor):
|
||||
print("Home Assistant requires at least Python {}.{}".format(
|
||||
req_major, req_minor))
|
||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -256,12 +297,14 @@ def setup_and_run_hass(config_dir: str,
|
||||
import webbrowser
|
||||
webbrowser.open(hass.config.api.base_url)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||
run_callback_threadsafe(
|
||||
hass.loop,
|
||||
hass.bus.async_listen_once,
|
||||
EVENT_HOMEASSISTANT_START, open_browser
|
||||
)
|
||||
|
||||
hass.start()
|
||||
exit_code = int(hass.block_till_stopped())
|
||||
|
||||
return exit_code
|
||||
return hass.exit_code
|
||||
|
||||
|
||||
def try_to_restart() -> None:
|
||||
@@ -308,6 +351,8 @@ def try_to_restart() -> None:
|
||||
|
||||
def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
monkey_patch_asyncio()
|
||||
|
||||
validate_python()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
+34
-21
@@ -118,11 +118,13 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
||||
|
||||
# Assumption: if a component does not depend on groups
|
||||
# it communicates with devices
|
||||
if 'group' not in getattr(component, 'DEPENDENCIES', []):
|
||||
if 'group' not in getattr(component, 'DEPENDENCIES', []) and \
|
||||
hass.pool.worker_count <= 10:
|
||||
hass.pool.add_worker()
|
||||
|
||||
hass.bus.fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN})
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -144,7 +146,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
if hasattr(component, 'CONFIG_SCHEMA'):
|
||||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, domain, config)
|
||||
return None
|
||||
|
||||
@@ -154,8 +156,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
# Validate component specific platform schema
|
||||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
log_exception(ex, domain, p_config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, domain, config)
|
||||
return None
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
@@ -175,7 +177,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
||||
try:
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.MultipleInvalid as ex:
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated)
|
||||
return None
|
||||
@@ -278,23 +280,29 @@ def from_config_dict(config: Dict[str, Any],
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
return hass
|
||||
# Setup in a thread to avoid blocking
|
||||
def component_setup():
|
||||
"""Set up a component."""
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
return hass
|
||||
|
||||
persistent_notification.setup(hass, config)
|
||||
persistent_notification.setup(hass, config)
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
|
||||
# Setup the components
|
||||
for domain in loader.load_order_components(components):
|
||||
_setup_component(hass, domain, config)
|
||||
# Setup the components
|
||||
for domain in loader.load_order_components(components):
|
||||
_setup_component(hass, domain, config)
|
||||
|
||||
hass.loop.run_until_complete(
|
||||
hass.loop.run_in_executor(None, component_setup)
|
||||
)
|
||||
return hass
|
||||
|
||||
|
||||
@@ -390,16 +398,21 @@ def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
||||
def log_exception(ex, domain, config):
|
||||
"""Generate log exception for config validation."""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||
.format(ex.path[-1], domain, domain,
|
||||
'->'.join('%s' % m for m in ex.path))
|
||||
else:
|
||||
message += humanize_error(config, ex)
|
||||
message += '{}.'.format(humanize_error(config, ex))
|
||||
|
||||
if hasattr(config, '__line__'):
|
||||
message += " (See {}:{})".format(config.__config_file__,
|
||||
config.__line__ or '?')
|
||||
message += " (See {}:{})".format(
|
||||
config.__config_file__, config.__line__ or '?')
|
||||
|
||||
if domain != 'homeassistant':
|
||||
message += (' Please check the docs at '
|
||||
'https://home-assistant.io/components/{}/'.format(domain))
|
||||
|
||||
_LOGGER.error(message)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
EnvisalinkDevice,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_CODE,
|
||||
CONF_PANIC,
|
||||
CONF_PARTITIONNAME,
|
||||
SIGNAL_PARTITION_UPDATE,
|
||||
SIGNAL_KEYPAD_UPDATE)
|
||||
@@ -26,6 +27,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
_code = discovery_info[CONF_CODE]
|
||||
_panic_type = discovery_info[CONF_PANIC]
|
||||
for part_num in _configured_partitions:
|
||||
_device_config_data = PARTITION_SCHEMA(
|
||||
_configured_partitions[part_num])
|
||||
@@ -33,6 +35,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
part_num,
|
||||
_device_config_data[CONF_PARTITIONNAME],
|
||||
_code,
|
||||
_panic_type,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
@@ -44,11 +47,13 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Represents the Envisalink-based alarm panel."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, partition_number, alarm_name, code, info, controller):
|
||||
def __init__(self, partition_number, alarm_name,
|
||||
code, panic_type, info, controller):
|
||||
"""Initialize the alarm panel."""
|
||||
from pydispatch import dispatcher
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
self._panic_type = panic_type
|
||||
_LOGGER.debug('Setting up alarm: ' + alarm_name)
|
||||
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
@@ -61,7 +66,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
def _update_callback(self, partition):
|
||||
"""Update HA state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.update_ha_state()
|
||||
self.hass.async_add_job(self.update_ha_state)
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
@@ -101,5 +106,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Not possible for us."""
|
||||
raise NotImplementedError()
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
||||
|
||||
@@ -28,7 +28,7 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
|
||||
@@ -26,7 +26,7 @@ DEFAULT_NAME = 'SimpliSafe'
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.positive_int,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ Support for Alexa skill service end point.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
"""
|
||||
import copy
|
||||
import enum
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -20,10 +23,49 @@ CONF_CARD = 'card'
|
||||
CONF_INTENTS = 'intents'
|
||||
CONF_SPEECH = 'speech'
|
||||
|
||||
CONF_TYPE = 'type'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_CONTENT = 'content'
|
||||
CONF_TEXT = 'text'
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
"""The Alexa speech types."""
|
||||
|
||||
plaintext = "PlainText"
|
||||
ssml = "SSML"
|
||||
|
||||
|
||||
class CardType(enum.Enum):
|
||||
"""The Alexa card types."""
|
||||
|
||||
simple = "Simple"
|
||||
link_account = "LinkAccount"
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
CONF_INTENTS: {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_CARD): {
|
||||
vol.Required(CONF_TYPE): cv.enum(CardType),
|
||||
vol.Required(CONF_TITLE): cv.template,
|
||||
vol.Required(CONF_CONTENT): cv.template,
|
||||
},
|
||||
vol.Optional(CONF_SPEECH): {
|
||||
vol.Required(CONF_TYPE): cv.enum(SpeechType),
|
||||
vol.Required(CONF_TEXT): cv.template,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
hass.wsgi.register_view(AlexaView(hass,
|
||||
@@ -42,6 +84,9 @@ class AlexaView(HomeAssistantView):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
|
||||
intents = copy.deepcopy(intents)
|
||||
template.attach(hass, intents)
|
||||
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(
|
||||
@@ -101,29 +146,15 @@ class AlexaView(HomeAssistantView):
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(SpeechType[speech['type']], speech['text'])
|
||||
response.add_speech(speech[CONF_TYPE], speech[CONF_TEXT])
|
||||
|
||||
if card is not None:
|
||||
response.add_card(CardType[card['type']], card['title'],
|
||||
card['content'])
|
||||
response.add_card(card[CONF_TYPE], card[CONF_TITLE],
|
||||
card[CONF_CONTENT])
|
||||
|
||||
return self.json(response)
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
"""The Alexa speech types."""
|
||||
|
||||
plaintext = "PlainText"
|
||||
ssml = "SSML"
|
||||
|
||||
|
||||
class CardType(enum.Enum):
|
||||
"""The Alexa card types."""
|
||||
|
||||
simple = "Simple"
|
||||
link_account = "LinkAccount"
|
||||
|
||||
|
||||
class AlexaResponse(object):
|
||||
"""Help generating the response for Alexa."""
|
||||
|
||||
@@ -153,8 +184,8 @@ class AlexaResponse(object):
|
||||
self.card = card
|
||||
return
|
||||
|
||||
card["title"] = self._render(title),
|
||||
card["content"] = self._render(content)
|
||||
card["title"] = title.render(self.variables)
|
||||
card["content"] = content.render(self.variables)
|
||||
self.card = card
|
||||
|
||||
def add_speech(self, speech_type, text):
|
||||
@@ -163,9 +194,12 @@ class AlexaResponse(object):
|
||||
|
||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||
|
||||
if isinstance(text, template.Template):
|
||||
text = text.render(self.variables)
|
||||
|
||||
self.speech = {
|
||||
'type': speech_type.value,
|
||||
key: self._render(text)
|
||||
key: text
|
||||
}
|
||||
|
||||
def add_reprompt(self, speech_type, text):
|
||||
@@ -176,7 +210,7 @@ class AlexaResponse(object):
|
||||
|
||||
self.reprompt = {
|
||||
'type': speech_type.value,
|
||||
key: self._render(text)
|
||||
key: text.render(self.variables)
|
||||
}
|
||||
|
||||
def as_dict(self):
|
||||
@@ -201,7 +235,3 @@ class AlexaResponse(object):
|
||||
'sessionAttributes': self.session_attributes,
|
||||
'response': response,
|
||||
}
|
||||
|
||||
def _render(self, template_string):
|
||||
"""Render a response, adding data from intent if available."""
|
||||
return template.render(self.hass, template_string, self.variables)
|
||||
|
||||
@@ -4,6 +4,7 @@ Rest API for Home Assistant.
|
||||
For more details about the RESTful API, please refer to the documentation at
|
||||
https://home-assistant.io/developers/api/
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
@@ -79,6 +80,7 @@ class APIEventStream(HomeAssistantView):
|
||||
if restrict:
|
||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||
|
||||
@asyncio.coroutine
|
||||
def forward_events(event):
|
||||
"""Forward events to the open request."""
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
@@ -376,8 +378,8 @@ class APITemplateView(HomeAssistantView):
|
||||
def post(self, request):
|
||||
"""Render a template."""
|
||||
try:
|
||||
return template.render(self.hass, request.json['template'],
|
||||
request.json.get('variables'))
|
||||
tpl = template.Template(request.json['template'], self.hass)
|
||||
return tpl.render(request.json.get('variables'))
|
||||
except TemplateError as ex:
|
||||
return self.json_message('Error rendering template: {}'.format(ex),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
from homeassistant.const import CONF_PORT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['PyMata==2.12']
|
||||
REQUIREMENTS = ['PyMata==2.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ Allow to setup simple automation rules via the config file.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/automation/
|
||||
"""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
import os
|
||||
@@ -23,27 +24,31 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.loader import get_platform
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DEPENDENCIES = ['group']
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
CONF_HIDE_ENTITY = 'hide_entity'
|
||||
|
||||
CONF_CONDITION = 'condition'
|
||||
CONF_ACTION = 'action'
|
||||
CONF_TRIGGER = 'trigger'
|
||||
CONF_CONDITION_TYPE = 'condition_type'
|
||||
CONF_INITIAL_STATE = 'initial_state'
|
||||
|
||||
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
|
||||
CONDITION_TYPE_AND = 'and'
|
||||
CONDITION_TYPE_OR = 'or'
|
||||
|
||||
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||
|
||||
METHOD_TRIGGER = 'trigger'
|
||||
METHOD_IF_ACTION = 'if_action'
|
||||
DEFAULT_HIDE_ENTITY = False
|
||||
DEFAULT_INITIAL_STATE = True
|
||||
|
||||
ATTR_LAST_TRIGGERED = 'last_triggered'
|
||||
ATTR_VARIABLES = 'variables'
|
||||
@@ -53,21 +58,14 @@ SERVICE_RELOAD = 'reload'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _platform_validator(method, schema):
|
||||
"""Generate platform validator for different steps."""
|
||||
def validator(config):
|
||||
"""Validate it is a valid platform."""
|
||||
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
|
||||
def _platform_validator(config):
|
||||
"""Validate it is a valid platform."""
|
||||
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
|
||||
|
||||
if not hasattr(platform, method):
|
||||
raise vol.Invalid('invalid method platform')
|
||||
if not hasattr(platform, 'TRIGGER_SCHEMA'):
|
||||
return config
|
||||
|
||||
if not hasattr(platform, schema):
|
||||
return config
|
||||
|
||||
return getattr(platform, schema)(config)
|
||||
|
||||
return validator
|
||||
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
||||
|
||||
_TRIGGER_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
@@ -76,32 +74,19 @@ _TRIGGER_SCHEMA = vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
|
||||
}, extra=vol.ALLOW_EXTRA),
|
||||
_platform_validator(METHOD_TRIGGER, 'TRIGGER_SCHEMA')
|
||||
_platform_validator
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
_CONDITION_SCHEMA = vol.Any(
|
||||
CONDITION_USE_TRIGGER_VALUES,
|
||||
vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.All(
|
||||
vol.Schema({
|
||||
CONF_PLATFORM: str,
|
||||
CONF_CONDITION: str,
|
||||
}, extra=vol.ALLOW_EXTRA),
|
||||
cv.has_at_least_one_key(CONF_PLATFORM, CONF_CONDITION),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
CONF_ALIAS: cv.string,
|
||||
vol.Optional(CONF_INITIAL_STATE,
|
||||
default=DEFAULT_INITIAL_STATE): cv.boolean,
|
||||
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
||||
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
||||
vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
|
||||
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
|
||||
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
})
|
||||
@@ -160,9 +145,11 @@ def reload(hass):
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the automation."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
||||
|
||||
success = _process_config(hass, config, component)
|
||||
success = run_coroutine_threadsafe(
|
||||
_async_process_config(hass, config, component), hass.loop).result()
|
||||
|
||||
if not success:
|
||||
return False
|
||||
@@ -170,22 +157,37 @@ def setup(hass, config):
|
||||
descriptions = conf_util.load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
def trigger_service_handler(service_call):
|
||||
"""Handle automation triggers."""
|
||||
for entity in component.extract_from_service(service_call):
|
||||
entity.trigger(service_call.data.get(ATTR_VARIABLES))
|
||||
hass.loop.create_task(entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES), True))
|
||||
|
||||
def service_handler(service_call):
|
||||
"""Handle automation service calls."""
|
||||
@asyncio.coroutine
|
||||
def turn_onoff_service_handler(service_call):
|
||||
"""Handle automation turn on/off service calls."""
|
||||
method = 'async_{}'.format(service_call.service)
|
||||
for entity in component.extract_from_service(service_call):
|
||||
getattr(entity, service_call.service)()
|
||||
hass.loop.create_task(getattr(entity, method)())
|
||||
|
||||
@asyncio.coroutine
|
||||
def toggle_service_handler(service_call):
|
||||
"""Handle automation toggle service calls."""
|
||||
for entity in component.extract_from_service(service_call):
|
||||
if entity.is_on:
|
||||
hass.loop.create_task(entity.async_turn_off())
|
||||
else:
|
||||
hass.loop.create_task(entity.async_turn_on())
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
"""Remove all automations and load new ones from config."""
|
||||
conf = component.prepare_reload()
|
||||
conf = yield from hass.loop.run_in_executor(
|
||||
None, component.prepare_reload)
|
||||
if conf is None:
|
||||
return
|
||||
_process_config(hass, conf, component)
|
||||
hass.loop.create_task(_async_process_config(hass, conf, component))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
descriptions.get(SERVICE_TRIGGER),
|
||||
@@ -195,8 +197,12 @@ def setup(hass, config):
|
||||
descriptions.get(SERVICE_RELOAD),
|
||||
schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE):
|
||||
hass.services.register(DOMAIN, service, service_handler,
|
||||
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||
descriptions.get(SERVICE_TOGGLE),
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
||||
hass.services.register(DOMAIN, service, turn_onoff_service_handler,
|
||||
descriptions.get(service),
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
@@ -206,15 +212,19 @@ def setup(hass, config):
|
||||
class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
def __init__(self, name, attach_triggers, cond_func, action):
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||
hidden):
|
||||
"""Initialize an automation entity."""
|
||||
self._name = name
|
||||
self._attach_triggers = attach_triggers
|
||||
self._detach_triggers = attach_triggers(self.trigger)
|
||||
self._async_attach_triggers = async_attach_triggers
|
||||
self._async_detach_triggers = None
|
||||
self._cond_func = cond_func
|
||||
self._action = action
|
||||
self._enabled = True
|
||||
self._async_action = async_action
|
||||
self._enabled = False
|
||||
self._last_triggered = None
|
||||
self._hidden = hidden
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -233,46 +243,75 @@ class AutomationEntity(ToggleEntity):
|
||||
ATTR_LAST_TRIGGERED: self._last_triggered
|
||||
}
|
||||
|
||||
@property
|
||||
def hidden(self) -> bool:
|
||||
"""Return True if the automation entity should be hidden from UIs."""
|
||||
return self._hidden
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self._enabled
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on."""
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on and update the state."""
|
||||
if self._enabled:
|
||||
return
|
||||
|
||||
self._detach_triggers = self._attach_triggers(self.trigger)
|
||||
self._enabled = True
|
||||
self.update_ha_state()
|
||||
yield from self.async_enable()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the entity off."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
self._detach_triggers()
|
||||
self._detach_triggers = None
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
self._enabled = False
|
||||
self.update_ha_state()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
|
||||
def trigger(self, variables):
|
||||
"""Trigger automation."""
|
||||
if self._cond_func(variables):
|
||||
self._action(variables)
|
||||
@asyncio.coroutine
|
||||
def async_trigger(self, variables, skip_condition=False):
|
||||
"""Trigger automation.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if skip_condition or self._cond_func(variables):
|
||||
yield from self._async_action(variables)
|
||||
self._last_triggered = utcnow()
|
||||
self.update_ha_state()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
|
||||
def remove(self):
|
||||
"""Remove automation from HASS."""
|
||||
self.turn_off()
|
||||
run_coroutine_threadsafe(self.async_turn_off(),
|
||||
self.hass.loop).result()
|
||||
super().remove()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_enable(self):
|
||||
"""Enable this automation entity.
|
||||
|
||||
def _process_config(hass, config, component):
|
||||
"""Process config and add automations."""
|
||||
success = False
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if self._enabled:
|
||||
return
|
||||
|
||||
self._async_detach_triggers = yield from self._async_attach_triggers(
|
||||
self.async_trigger)
|
||||
self._enabled = True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_process_config(hass, config, component):
|
||||
"""Process config and add automations.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
entities = []
|
||||
tasks = []
|
||||
|
||||
for config_key in extract_domain_configs(config, DOMAIN):
|
||||
conf = config[config_key]
|
||||
@@ -281,10 +320,13 @@ def _process_config(hass, config, component):
|
||||
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
|
||||
list_no)
|
||||
|
||||
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)
|
||||
hidden = config_block[CONF_HIDE_ENTITY]
|
||||
|
||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
||||
name)
|
||||
|
||||
if CONF_CONDITION in config_block:
|
||||
cond_func = _process_if(hass, config, config_block)
|
||||
cond_func = _async_process_if(hass, config, config_block)
|
||||
|
||||
if cond_func is None:
|
||||
continue
|
||||
@@ -293,100 +335,78 @@ def _process_config(hass, config, component):
|
||||
"""Condition will always pass."""
|
||||
return True
|
||||
|
||||
attach_triggers = partial(_process_trigger, hass, config,
|
||||
config_block.get(CONF_TRIGGER, []), name)
|
||||
entity = AutomationEntity(name, attach_triggers, cond_func, action)
|
||||
component.add_entities((entity,))
|
||||
success = True
|
||||
async_attach_triggers = partial(
|
||||
_async_process_trigger, hass, config,
|
||||
config_block.get(CONF_TRIGGER, []), name)
|
||||
entity = AutomationEntity(name, async_attach_triggers, cond_func,
|
||||
action, hidden)
|
||||
if config_block[CONF_INITIAL_STATE]:
|
||||
tasks.append(hass.loop.create_task(entity.async_enable()))
|
||||
entities.append(entity)
|
||||
|
||||
return success
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, component.add_entities, entities)
|
||||
|
||||
return len(entities) > 0
|
||||
|
||||
|
||||
def _get_action(hass, config, name):
|
||||
def _async_get_action(hass, config, name):
|
||||
"""Return an action based on a configuration."""
|
||||
script_obj = script.Script(hass, config, name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def action(variables=None):
|
||||
"""Action to be executed."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
script_obj.run(variables)
|
||||
logbook.async_log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
hass.loop.create_task(script_obj.async_run(variables))
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _process_if(hass, config, p_config):
|
||||
def _async_process_if(hass, config, p_config):
|
||||
"""Process if checks."""
|
||||
cond_type = p_config.get(CONF_CONDITION_TYPE,
|
||||
DEFAULT_CONDITION_TYPE).lower()
|
||||
|
||||
# Deprecated since 0.19 - 5/5/2016
|
||||
if cond_type != DEFAULT_CONDITION_TYPE:
|
||||
_LOGGER.warning('Using condition_type: "or" is deprecated. Please use '
|
||||
'"condition: or" instead.')
|
||||
|
||||
if_configs = p_config.get(CONF_CONDITION)
|
||||
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
|
||||
|
||||
if use_trigger:
|
||||
if_configs = p_config[CONF_TRIGGER]
|
||||
|
||||
checks = []
|
||||
for if_config in if_configs:
|
||||
# Deprecated except for used by use_trigger_values
|
||||
# since 0.19 - 5/5/2016
|
||||
if CONF_PLATFORM in if_config:
|
||||
if not use_trigger:
|
||||
_LOGGER.warning("Please switch your condition configuration "
|
||||
"to use 'condition' instead of 'platform'.")
|
||||
if_config = dict(if_config)
|
||||
if_config[CONF_CONDITION] = if_config.pop(CONF_PLATFORM)
|
||||
|
||||
# To support use_trigger_values with state trigger accepting
|
||||
# multiple entity_ids to monitor.
|
||||
if_entity_id = if_config.get(ATTR_ENTITY_ID)
|
||||
if isinstance(if_entity_id, list) and len(if_entity_id) == 1:
|
||||
if_config[ATTR_ENTITY_ID] = if_entity_id[0]
|
||||
|
||||
try:
|
||||
checks.append(condition.from_config(if_config))
|
||||
checks.append(condition.async_from_config(if_config, False))
|
||||
except HomeAssistantError as ex:
|
||||
# Invalid conditions are allowed if we base it on trigger
|
||||
if use_trigger:
|
||||
_LOGGER.warning('Ignoring invalid condition: %s', ex)
|
||||
else:
|
||||
_LOGGER.warning('Invalid condition: %s', ex)
|
||||
return None
|
||||
_LOGGER.warning('Invalid condition: %s', ex)
|
||||
return None
|
||||
|
||||
if cond_type == CONDITION_TYPE_AND:
|
||||
def if_action(variables=None):
|
||||
"""AND all conditions."""
|
||||
return all(check(hass, variables) for check in checks)
|
||||
else:
|
||||
def if_action(variables=None):
|
||||
"""OR all conditions."""
|
||||
return any(check(hass, variables) for check in checks)
|
||||
def if_action(variables=None):
|
||||
"""AND all conditions."""
|
||||
return all(check(hass, variables) for check in checks)
|
||||
|
||||
return if_action
|
||||
|
||||
|
||||
def _process_trigger(hass, config, trigger_configs, name, action):
|
||||
"""Setup the triggers."""
|
||||
@asyncio.coroutine
|
||||
def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
"""Setup the triggers.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
removes = []
|
||||
|
||||
for conf in trigger_configs:
|
||||
platform = _resolve_platform(METHOD_TRIGGER, hass, config,
|
||||
conf.get(CONF_PLATFORM))
|
||||
if platform is None:
|
||||
continue
|
||||
platform = yield from hass.loop.run_in_executor(
|
||||
None, prepare_setup_platform, hass, config, DOMAIN,
|
||||
conf.get(CONF_PLATFORM))
|
||||
|
||||
remove = platform.trigger(hass, conf, action)
|
||||
if platform is None:
|
||||
return None
|
||||
|
||||
remove = platform.async_trigger(hass, conf, action)
|
||||
|
||||
if not remove:
|
||||
_LOGGER.error("Error setting up rule %s", name)
|
||||
_LOGGER.error("Error setting up trigger %s", name)
|
||||
continue
|
||||
|
||||
_LOGGER.info("Initialized rule %s", name)
|
||||
_LOGGER.info("Initialized trigger %s", name)
|
||||
removes.append(remove)
|
||||
|
||||
if not removes:
|
||||
@@ -398,17 +418,3 @@ def _process_trigger(hass, config, trigger_configs, name, action):
|
||||
remove()
|
||||
|
||||
return remove_triggers
|
||||
|
||||
|
||||
def _resolve_platform(method, hass, config, platform):
|
||||
"""Find the automation platform."""
|
||||
if platform is None:
|
||||
return None
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
|
||||
|
||||
if platform is None or not hasattr(platform, method):
|
||||
_LOGGER.error("Unknown automation platform specified for %s: %s",
|
||||
method, platform)
|
||||
return None
|
||||
|
||||
return platform
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -23,20 +24,21 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data = config.get(CONF_EVENT_DATA)
|
||||
|
||||
@callback
|
||||
def handle_event(event):
|
||||
"""Listen for events and calls the action when data matches."""
|
||||
if not event_data or all(val == event.data.get(key) for key, val
|
||||
in event_data.items()):
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
|
||||
return hass.bus.listen(event_type, handle_event)
|
||||
return hass.bus.async_listen(event_type, handle_event)
|
||||
|
||||
@@ -6,6 +6,7 @@ at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -21,15 +22,16 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
topic = config.get(CONF_TOPIC)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
|
||||
@callback
|
||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||
"""Listen for MQTT messages."""
|
||||
if payload is None or payload == msg_payload:
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'mqtt',
|
||||
'topic': msg_topic,
|
||||
@@ -38,4 +40,4 @@ def trigger(hass, config, action):
|
||||
}
|
||||
})
|
||||
|
||||
return mqtt.subscribe(hass, topic, mqtt_automation_listener)
|
||||
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
||||
|
||||
@@ -8,10 +8,11 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
@@ -25,14 +26,16 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
if to_s is None:
|
||||
@@ -48,19 +51,19 @@ def trigger(hass, config, action):
|
||||
}
|
||||
|
||||
# If new one doesn't match, nothing to do
|
||||
if not condition.numeric_state(
|
||||
if not condition.async_numeric_state(
|
||||
hass, to_s, below, above, value_template, variables):
|
||||
return
|
||||
|
||||
# Only match if old didn't exist or existed but didn't match
|
||||
# Written as: skip if old one did exist and matched
|
||||
if from_s is not None and condition.numeric_state(
|
||||
if from_s is not None and condition.async_numeric_state(
|
||||
hass, from_s, below, above, value_template, variables):
|
||||
return
|
||||
|
||||
variables['trigger']['from_state'] = from_s
|
||||
variables['trigger']['to_state'] = to_s
|
||||
|
||||
action(variables)
|
||||
hass.async_run_job(action, variables)
|
||||
|
||||
return track_state_change(hass, entity_id, state_automation_listener)
|
||||
return async_track_state_change(hass, entity_id, state_automation_listener)
|
||||
|
||||
@@ -6,9 +6,11 @@ at https://home-assistant.io/components/automation/#state-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
||||
from homeassistant.helpers.event import track_state_change, track_point_in_time
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_point_in_utc_time)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
@@ -32,22 +34,23 @@ TRIGGER_SCHEMA = vol.All(
|
||||
)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
||||
time_delta = config.get(CONF_FOR)
|
||||
remove_state_for_cancel = None
|
||||
remove_state_for_listener = None
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_state_for_listener = None
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal remove_state_for_cancel, remove_state_for_listener
|
||||
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
||||
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'state',
|
||||
'entity_id': entity,
|
||||
@@ -61,35 +64,37 @@ def trigger(hass, config, action):
|
||||
call_action()
|
||||
return
|
||||
|
||||
@callback
|
||||
def state_for_listener(now):
|
||||
"""Fire on state changes after a delay and calls action."""
|
||||
remove_state_for_cancel()
|
||||
async_remove_state_for_cancel()
|
||||
call_action()
|
||||
|
||||
@callback
|
||||
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
||||
"""Fire on changes and cancel for listener if changed."""
|
||||
if inner_to_s.state == to_s.state:
|
||||
return
|
||||
remove_state_for_listener()
|
||||
remove_state_for_cancel()
|
||||
async_remove_state_for_listener()
|
||||
async_remove_state_for_cancel()
|
||||
|
||||
remove_state_for_listener = track_point_in_time(
|
||||
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||
|
||||
remove_state_for_cancel = track_state_change(
|
||||
async_remove_state_for_cancel = async_track_state_change(
|
||||
hass, entity, state_for_cancel_listener)
|
||||
|
||||
unsub = track_state_change(hass, entity_id, state_automation_listener,
|
||||
from_state, to_state)
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
||||
def remove():
|
||||
"""Remove state listeners."""
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
# pylint: disable=not-callable
|
||||
if remove_state_for_cancel is not None:
|
||||
remove_state_for_cancel()
|
||||
if async_remove_state_for_cancel is not None:
|
||||
async_remove_state_for_cancel()
|
||||
|
||||
if remove_state_for_listener is not None:
|
||||
remove_state_for_listener()
|
||||
if async_remove_state_for_listener is not None:
|
||||
async_remove_state_for_listener()
|
||||
|
||||
return remove
|
||||
return async_remove
|
||||
|
||||
@@ -9,9 +9,10 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
|
||||
from homeassistant.helpers.event import track_sunrise, track_sunset
|
||||
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['sun']
|
||||
@@ -25,14 +26,15 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
offset = config.get(CONF_OFFSET)
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'sun',
|
||||
'event': event,
|
||||
@@ -42,6 +44,6 @@ def trigger(hass, config, action):
|
||||
|
||||
# Do something to call action
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
return track_sunrise(hass, call_action, offset)
|
||||
return async_track_sunrise(hass, call_action, offset)
|
||||
else:
|
||||
return track_sunset(hass, call_action, offset)
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
|
||||
@@ -8,10 +8,10 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
@@ -23,22 +23,24 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = hass
|
||||
|
||||
# Local variable to keep track of if the action has already been triggered
|
||||
already_triggered = False
|
||||
|
||||
@callback
|
||||
def state_changed_listener(entity_id, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal already_triggered
|
||||
template_result = condition.template(hass, value_template)
|
||||
template_result = condition.async_template(hass, value_template)
|
||||
|
||||
# Check to see if template returns true
|
||||
if template_result and not already_triggered:
|
||||
already_triggered = True
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
@@ -49,4 +51,5 @@ def trigger(hass, config, action):
|
||||
elif not template_result:
|
||||
already_triggered = False
|
||||
|
||||
return track_state_change(hass, MATCH_ALL, state_changed_listener)
|
||||
return async_track_state_change(hass, value_template.extract_entities(),
|
||||
state_changed_listener)
|
||||
|
||||
@@ -8,9 +8,10 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
CONF_HOURS = "hours"
|
||||
CONF_MINUTES = "minutes"
|
||||
@@ -28,7 +29,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
CONF_SECONDS, CONF_AFTER))
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AFTER in config:
|
||||
after = config.get(CONF_AFTER)
|
||||
@@ -38,14 +39,15 @@ def trigger(hass, config, action):
|
||||
minutes = config.get(CONF_MINUTES)
|
||||
seconds = config.get(CONF_SECONDS)
|
||||
|
||||
@callback
|
||||
def time_automation_listener(now):
|
||||
"""Listen for time changes and calls action."""
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'time',
|
||||
'now': now,
|
||||
},
|
||||
})
|
||||
|
||||
return track_time_change(hass, time_automation_listener,
|
||||
hour=hours, minute=minutes, second=seconds)
|
||||
return async_track_time_change(hass, time_automation_listener,
|
||||
hour=hours, minute=minutes, second=seconds)
|
||||
|
||||
@@ -6,9 +6,10 @@ at https://home-assistant.io/components/automation/#zone-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM)
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers import (
|
||||
condition, config_validation as cv, location)
|
||||
|
||||
@@ -25,12 +26,13 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
@callback
|
||||
def zone_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
if from_s and not location.has_location(from_s) or \
|
||||
@@ -47,7 +49,7 @@ def trigger(hass, config, action):
|
||||
# pylint: disable=too-many-boolean-expressions
|
||||
if event == EVENT_ENTER and not from_match and to_match or \
|
||||
event == EVENT_LEAVE and from_match and not to_match:
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'zone',
|
||||
'entity_id': entity,
|
||||
@@ -58,5 +60,5 @@ def trigger(hass, config, action):
|
||||
},
|
||||
})
|
||||
|
||||
return track_state_change(hass, entity_id, zone_automation_listener,
|
||||
MATCH_ALL, MATCH_ALL)
|
||||
return async_track_state_change(hass, entity_id, zone_automation_listener,
|
||||
MATCH_ALL, MATCH_ALL)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for exposed aREST RESTful API of a device.
|
||||
Support for an exposed aREST RESTful API of a device.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.arest/
|
||||
@@ -8,34 +8,32 @@ import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
SENSOR_CLASSES)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_SENSOR_CLASS)
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
CONF_RESOURCE = 'resource'
|
||||
CONF_PIN = 'pin'
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_PIN): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the aREST binary sensor."""
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
pin = config.get(CONF_PIN)
|
||||
|
||||
sensor_class = config.get('sensor_class')
|
||||
if sensor_class not in SENSOR_CLASSES:
|
||||
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
|
||||
sensor_class = None
|
||||
|
||||
if None in (resource, pin):
|
||||
_LOGGER.error('Not all required config keys present: %s',
|
||||
', '.join((CONF_RESOURCE, CONF_PIN)))
|
||||
return False
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
|
||||
try:
|
||||
response = requests.get(resource, timeout=10).json()
|
||||
@@ -52,11 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
arest = ArestData(resource, pin)
|
||||
|
||||
add_devices([ArestBinarySensor(
|
||||
arest,
|
||||
resource,
|
||||
config.get('name', response['name']),
|
||||
sensor_class,
|
||||
pin)])
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
sensor_class, pin)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
|
||||
@@ -15,7 +15,6 @@ from homeassistant.components.sensor.command_line import CommandSensorData
|
||||
from homeassistant.const import (
|
||||
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSOR_CLASS, CONF_COMMAND)
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -45,7 +44,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
payload_on = config.get(CONF_PAYLOAD_ON)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
data = CommandSensorData(command)
|
||||
|
||||
add_devices([CommandBinarySensor(
|
||||
@@ -91,8 +91,8 @@ class CommandBinarySensor(BinarySensorDevice):
|
||||
value = self.data.value
|
||||
|
||||
if self._value_template is not None:
|
||||
value = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, value, False)
|
||||
value = self._value_template.render_with_possible_json_value(
|
||||
value, False)
|
||||
if value == self._payload_on:
|
||||
self._state = True
|
||||
elif value == self._payload_off:
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
Support for monitoring the state of Digital Ocean droplets.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.digital_ocean/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.digital_ocean import (
|
||||
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
||||
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||
ATTR_REGION, ATTR_VCPUS)
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Droplet'
|
||||
DEFAULT_SENSOR_CLASS = 'motion'
|
||||
DEPENDENCIES = ['digital_ocean']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Digital Ocean droplet sensor."""
|
||||
digital_ocean = get_component('digital_ocean')
|
||||
droplets = config.get(CONF_DROPLETS)
|
||||
|
||||
dev = []
|
||||
for droplet in droplets:
|
||||
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
|
||||
dev.append(DigitalOceanBinarySensor(
|
||||
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Digital Ocean droplet sensor."""
|
||||
|
||||
def __init__(self, do, droplet_id):
|
||||
"""Initialize a new Digital Ocean sensor."""
|
||||
self._digital_ocean = do
|
||||
self._droplet_id = droplet_id
|
||||
self._state = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self.data.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.data.status == 'active'
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEFAULT_SENSOR_CLASS
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes of the Digital Ocean droplet."""
|
||||
return {
|
||||
ATTR_CREATED_AT: self.data.created_at,
|
||||
ATTR_DROPLET_ID: self.data.id,
|
||||
ATTR_DROPLET_NAME: self.data.name,
|
||||
ATTR_FEATURES: self.data.features,
|
||||
ATTR_IPV4_ADDRESS: self.data.ip_address,
|
||||
ATTR_IPV6_ADDRESS: self.data.ip_v6_address,
|
||||
ATTR_MEMORY: self.data.memory,
|
||||
ATTR_REGION: self.data.region['name'],
|
||||
ATTR_VCPUS: self.data.vcpus,
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Update state of sensor."""
|
||||
self._digital_ocean.update()
|
||||
|
||||
for droplet in self._digital_ocean.data:
|
||||
if droplet.id == self._droplet_id:
|
||||
self.data = droplet
|
||||
@@ -68,4 +68,4 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
def _update_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self.update_ha_state()
|
||||
self.hass.async_add_job(self.update_ha_state)
|
||||
|
||||
@@ -10,13 +10,17 @@ from os import path
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
PLATFORM_SCHEMA, DOMAIN)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DOMAIN)
|
||||
from homeassistant.components.ffmpeg import (
|
||||
get_binary, run_test, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME,
|
||||
ATTR_ENTITY_ID)
|
||||
|
||||
REQUIREMENTS = ["ha-ffmpeg==0.10"]
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RESTART = 'ffmpeg_restart'
|
||||
|
||||
@@ -29,10 +33,6 @@ MAP_FFMPEG_BIN = [
|
||||
]
|
||||
|
||||
CONF_TOOL = 'tool'
|
||||
CONF_INPUT = 'input'
|
||||
CONF_FFMPEG_BIN = 'ffmpeg_bin'
|
||||
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
|
||||
CONF_OUTPUT = 'output'
|
||||
CONF_PEAK = 'peak'
|
||||
CONF_DURATION = 'duration'
|
||||
CONF_RESET = 'reset'
|
||||
@@ -40,11 +40,12 @@ CONF_CHANGES = 'changes'
|
||||
CONF_REPEAT = 'repeat'
|
||||
CONF_REPEAT_TIME = 'repeat_time'
|
||||
|
||||
DEFAULT_NAME = 'FFmpeg'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN),
|
||||
vol.Required(CONF_INPUT): cv.string,
|
||||
vol.Optional(CONF_FFMPEG_BIN, default="ffmpeg"): cv.string,
|
||||
vol.Optional(CONF_NAME, default="FFmpeg"): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_OUTPUT): cv.string,
|
||||
vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int),
|
||||
@@ -65,16 +66,25 @@ SERVICE_RESTART_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def restart(hass, entity_id=None):
|
||||
"""Restart a ffmpeg process on entity."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_RESTART, data)
|
||||
|
||||
|
||||
# list of all ffmpeg sensors
|
||||
DEVICES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Create the binary sensor."""
|
||||
from haffmpeg import SensorNoise, SensorMotion
|
||||
|
||||
# check source
|
||||
if not run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
|
||||
# generate sensor object
|
||||
if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE:
|
||||
entity = FFmpegNoise(SensorNoise, config)
|
||||
else:
|
||||
@@ -88,7 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
# exists service?
|
||||
if hass.services.has_service(DOMAIN, SERVICE_RESTART):
|
||||
return True
|
||||
return
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
@@ -105,13 +115,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
_devices = DEVICES
|
||||
|
||||
for device in _devices:
|
||||
device.reset_ffmpeg()
|
||||
device.restart_ffmpeg()
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_RESTART,
|
||||
_service_handle_restart,
|
||||
descriptions.get(SERVICE_RESTART),
|
||||
schema=SERVICE_RESTART_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
class FFmpegBinarySensor(BinarySensorDevice):
|
||||
@@ -122,7 +131,7 @@ class FFmpegBinarySensor(BinarySensorDevice):
|
||||
self._state = False
|
||||
self._config = config
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._ffmpeg = ffobj(config.get(CONF_FFMPEG_BIN), self._callback)
|
||||
self._ffmpeg = ffobj(get_binary(), self._callback)
|
||||
|
||||
self._start_ffmpeg(config)
|
||||
|
||||
@@ -139,7 +148,7 @@ class FFmpegBinarySensor(BinarySensorDevice):
|
||||
"""For STOP event to shutdown ffmpeg."""
|
||||
self._ffmpeg.close()
|
||||
|
||||
def reset_ffmpeg(self):
|
||||
def restart_ffmpeg(self):
|
||||
"""Restart ffmpeg with new config."""
|
||||
self._ffmpeg.close()
|
||||
self._start_ffmpeg(self._config)
|
||||
|
||||
@@ -16,6 +16,7 @@ DEPENDENCIES = ['homematic']
|
||||
SENSOR_TYPES_CLASS = {
|
||||
"Remote": None,
|
||||
"ShutterContact": "opening",
|
||||
"IPShutterContact": "opening",
|
||||
"Smoke": "smoke",
|
||||
"SmokeV2": "smoke",
|
||||
"Motion": "motion",
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
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 logging
|
||||
from typing import Callable # noqa
|
||||
|
||||
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
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
False: STATE_OFF,
|
||||
True: STATE_ON,
|
||||
}
|
||||
|
||||
UOM = ['2', '78']
|
||||
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Setup the ISY994 binary sensor platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error('A connection has not been made to the ISY controller.')
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
|
||||
states=STATES):
|
||||
devices.append(ISYBinarySensorDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
devices.append(ISYBinarySensorProgram(program.name, status))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor device."""
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 binary sensor device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
|
||||
@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
|
||||
@@ -5,17 +5,14 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.knx/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.knx import (
|
||||
KNXConfig, KNXGroupAddress)
|
||||
from homeassistant.components.knx import (KNXConfig, KNXGroupAddress)
|
||||
|
||||
DEPENDENCIES = ["knx"]
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the KNX binary sensor platform."""
|
||||
add_entities([
|
||||
KNXSwitch(hass, KNXConfig(config))
|
||||
])
|
||||
add_devices([KNXSwitch(hass, KNXConfig(config))])
|
||||
|
||||
|
||||
class KNXSwitch(KNXGroupAddress, BinarySensorDevice):
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Support for Modbus Coil sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.modbus/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.modbus as modbus
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['modbus']
|
||||
|
||||
CONF_COIL = "coil"
|
||||
CONF_COILS = "coils"
|
||||
CONF_SLAVE = "slave"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COILS): [{
|
||||
vol.Required(CONF_COIL): cv.positive_int,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_SLAVE): cv.positive_int
|
||||
}]
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Modbus binary sensors."""
|
||||
sensors = []
|
||||
for coil in config.get(CONF_COILS):
|
||||
sensors.append(ModbusCoilSensor(
|
||||
coil.get(CONF_NAME),
|
||||
coil.get(CONF_SLAVE),
|
||||
coil.get(CONF_COIL)))
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
class ModbusCoilSensor(BinarySensorDevice):
|
||||
"""Modbus coil sensor."""
|
||||
|
||||
def __init__(self, name, slave, coil):
|
||||
"""Initialize the modbus coil sensor."""
|
||||
self._name = name
|
||||
self._slave = int(slave) if slave else None
|
||||
self._coil = int(coil)
|
||||
self._value = None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._value
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the sensor."""
|
||||
result = modbus.HUB.read_coils(self._slave, self._coil, 1)
|
||||
self._value = result.bits[0]
|
||||
@@ -15,7 +15,6 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
|
||||
CONF_SENSOR_CLASS)
|
||||
from homeassistant.components.mqtt import (CONF_STATE_TOPIC, CONF_QOS)
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -37,6 +36,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the MQTT binary sensor."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
add_devices([MqttBinarySensor(
|
||||
hass,
|
||||
config.get(CONF_NAME),
|
||||
@@ -45,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template
|
||||
)])
|
||||
|
||||
|
||||
@@ -68,8 +70,8 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = template.render_with_possible_json_value(
|
||||
hass, value_template, payload)
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._payload_on:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
@@ -1,41 +1,56 @@
|
||||
"""
|
||||
Support for exposing nx584 elements as sensors.
|
||||
Support for exposing NX584 elements as sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.nx584/
|
||||
https://home-assistant.io/components/binary_sensor.nx584/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
SENSOR_CLASSES, BinarySensorDevice)
|
||||
SENSOR_CLASSES, BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_EXCLUDE_ZONES = 'exclude_zones'
|
||||
CONF_ZONE_TYPES = 'zone_types'
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = '5007'
|
||||
DEFAULT_SSL = False
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_EXCLUDE_ZONES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup nx584 binary sensor platform."""
|
||||
"""Setup the NX584 binary sensor platform."""
|
||||
from nx584 import client as nx584_client
|
||||
|
||||
host = config.get('host', 'localhost:5007')
|
||||
exclude = config.get('exclude_zones', [])
|
||||
zone_types = config.get('zone_types', {})
|
||||
|
||||
if not all(isinstance(zone, int) for zone in exclude):
|
||||
_LOGGER.error('Invalid excluded zone specified (use zone number)')
|
||||
return False
|
||||
|
||||
if not all(isinstance(zone, int) and ztype in SENSOR_CLASSES
|
||||
for zone, ztype in zone_types.items()):
|
||||
_LOGGER.error('Invalid zone_types entry')
|
||||
return False
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
exclude = config.get(CONF_EXCLUDE_ZONES)
|
||||
zone_types = config.get(CONF_ZONE_TYPES)
|
||||
|
||||
try:
|
||||
client = nx584_client.Client('http://%s' % host)
|
||||
client = nx584_client.Client('http://{}:{}'.format(host, port))
|
||||
zones = client.list_zones()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to NX584: %s', str(ex))
|
||||
@@ -43,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
version = [int(v) for v in client.get_version().split('.')]
|
||||
if version < [1, 1]:
|
||||
_LOGGER.error('NX584 is too old to use for sensors (>=0.2 required)')
|
||||
_LOGGER.error("NX584 is too old to use for sensors (>=0.2 required)")
|
||||
return False
|
||||
|
||||
zone_sensors = {
|
||||
@@ -57,13 +72,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
watcher = NX584Watcher(client, zone_sensors)
|
||||
watcher.start()
|
||||
else:
|
||||
_LOGGER.warning('No zones found on NX584')
|
||||
|
||||
_LOGGER.warning("No zones found on NX584")
|
||||
return True
|
||||
|
||||
|
||||
class NX584ZoneSensor(BinarySensorDevice):
|
||||
"""Represents a NX584 zone as a sensor."""
|
||||
"""Representation of a NX584 zone as a sensor."""
|
||||
|
||||
def __init__(self, zone, zone_type):
|
||||
"""Initialize the nx594 binary sensor."""
|
||||
@@ -96,7 +110,7 @@ class NX584Watcher(threading.Thread):
|
||||
"""Event listener thread to process NX584 events."""
|
||||
|
||||
def __init__(self, client, zone_sensors):
|
||||
"""Initialize nx584 watcher thread."""
|
||||
"""Initialize NX584 watcher thread."""
|
||||
super(NX584Watcher, self).__init__()
|
||||
self.daemon = True
|
||||
self._client = client
|
||||
@@ -130,5 +144,5 @@ class NX584Watcher(threading.Thread):
|
||||
try:
|
||||
self._run()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('Failed to reach NX584 server')
|
||||
_LOGGER.error("Failed to reach NX584 server")
|
||||
time.sleep(10)
|
||||
|
||||
@@ -5,16 +5,19 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.sensor.rest import RestData
|
||||
from homeassistant.const import (
|
||||
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
|
||||
CONF_SENSOR_CLASS, CONF_VERIFY_SSL)
|
||||
from homeassistant.helpers import template
|
||||
CONF_SENSOR_CLASS, CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
|
||||
HTTP_DIGEST_AUTHENTICATION)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -25,16 +28,21 @@ DEFAULT_VERIFY_SSL = True
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_AUTHENTICATION):
|
||||
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
|
||||
vol.Optional(CONF_HEADERS): cv.string,
|
||||
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
# pylint: disable=unused-variable, too-many-locals
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the REST binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
@@ -42,10 +50,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
method = config.get(CONF_METHOD)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
headers = json.loads(config.get(CONF_HEADERS, '{}'))
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
rest = RestData(method, resource, payload, verify_ssl)
|
||||
if username and password:
|
||||
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
|
||||
auth = HTTPDigestAuth(username, password)
|
||||
else:
|
||||
auth = HTTPBasicAuth(username, password)
|
||||
else:
|
||||
auth = None
|
||||
|
||||
rest = RestData(method, resource, auth, headers, payload, verify_ssl)
|
||||
rest.update()
|
||||
|
||||
if rest.data is None:
|
||||
@@ -88,8 +109,8 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
if self._value_template is not None:
|
||||
response = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
response = self._value_template.render_with_possible_json_value(
|
||||
self.rest.data, False)
|
||||
|
||||
try:
|
||||
return bool(int(response))
|
||||
|
||||
@@ -6,16 +6,37 @@ https://home-assistant.io/components/binary_sensor.rpi_gpio/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BOUNCETIME = 'bouncetime'
|
||||
CONF_INVERT_LOGIC = 'invert_logic'
|
||||
CONF_PORTS = 'ports'
|
||||
CONF_PULL_MODE = 'pull_mode'
|
||||
|
||||
DEFAULT_PULL_MODE = "UP"
|
||||
DEFAULT_BOUNCETIME = 50
|
||||
DEFAULT_INVERT_LOGIC = False
|
||||
DEFAULT_PULL_MODE = 'UP'
|
||||
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_SENSORS_SCHEMA = vol.Schema({
|
||||
cv.positive_int: cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PORTS): _SENSORS_SCHEMA,
|
||||
vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int,
|
||||
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
|
||||
vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Support for SleepIQ sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.sleepiq/
|
||||
"""
|
||||
from homeassistant.components import sleepiq
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = ['sleepiq']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the SleepIQ sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = sleepiq.DATA
|
||||
data.update()
|
||||
|
||||
dev = list()
|
||||
for bed_id, _ in data.beds.items():
|
||||
for side in sleepiq.SIDES:
|
||||
dev.append(IsInBedBinarySensor(data, bed_id, side))
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
|
||||
"""Implementation of a SleepIQ presence sensor."""
|
||||
|
||||
def __init__(self, sleepiq_data, bed_id, side):
|
||||
"""Initialize the sensor."""
|
||||
sleepiq.SleepIQSensor.__init__(self, sleepiq_data, bed_id, side)
|
||||
self.type = sleepiq.IS_IN_BED
|
||||
self._state = None
|
||||
self._name = sleepiq.SENSOR_TYPES[self.type]
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state is True
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return "occupancy"
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from SleepIQ and updates the states."""
|
||||
sleepiq.SleepIQSensor.update(self)
|
||||
self._state = self.side.is_in_bed
|
||||
@@ -4,18 +4,19 @@ Support for exposing a templated binary sensor.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.template/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||
SENSOR_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, MATCH_ALL, CONF_VALUE_TEMPLATE,
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -25,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(ATTR_ENTITY_ID, default=MATCH_ALL): cv.entity_ids,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA
|
||||
})
|
||||
|
||||
@@ -40,10 +41,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
for device, device_config in config[CONF_SENSORS].items():
|
||||
value_template = device_config[CONF_VALUE_TEMPLATE]
|
||||
entity_ids = device_config[ATTR_ENTITY_ID]
|
||||
entity_ids = (device_config.get(ATTR_ENTITY_ID) or
|
||||
value_template.extract_entities())
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||
sensor_class = device_config.get(CONF_SENSOR_CLASS)
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
sensors.append(
|
||||
BinarySensorTemplate(
|
||||
hass,
|
||||
@@ -78,9 +83,10 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
self.update()
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.update_ha_state(True)
|
||||
hass.loop.create_task(self.async_update_ha_state(True))
|
||||
|
||||
track_state_change(hass, entity_ids, template_bsensor_state_listener)
|
||||
|
||||
@@ -104,11 +110,11 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
try:
|
||||
self._state = template.render(
|
||||
self.hass, self._template).lower() == 'true'
|
||||
self._state = self._template.async_render().lower() == 'true'
|
||||
except TemplateError as ex:
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
A sensor that monitors trands in other components.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.template/
|
||||
https://home-assistant.io/components/sensor.trend/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
@@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the template sensors."""
|
||||
"""Setup the trend sensors."""
|
||||
sensors = []
|
||||
|
||||
for device, device_config in config[CONF_SENSORS].items():
|
||||
@@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class SensorTrend(BinarySensorDevice):
|
||||
"""Representation of a Template Sensor."""
|
||||
"""Representation of a trend Sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
def __init__(self, hass, device_id, friendly_name,
|
||||
@@ -90,14 +90,14 @@ class SensorTrend(BinarySensorDevice):
|
||||
|
||||
self.update()
|
||||
|
||||
def template_sensor_state_listener(entity, old_state, new_state):
|
||||
def trend_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.from_state = old_state
|
||||
self.to_state = new_state
|
||||
self.update_ha_state(True)
|
||||
|
||||
track_state_change(hass, target_entity,
|
||||
template_sensor_state_listener)
|
||||
trend_sensor_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"""
|
||||
Support for Wink sensors.
|
||||
Support for Wink binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/sensor.wink/
|
||||
at https://home-assistant.io/components/binary_sensor.wink/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.13', 'pubnub==3.8.2']
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
@@ -21,7 +19,11 @@ SENSOR_TYPES = {
|
||||
"brightness": "light",
|
||||
"vibration": "vibration",
|
||||
"loudness": "sound",
|
||||
"liquid_detected": "moisture"
|
||||
"liquid_detected": "moisture",
|
||||
"motion": "motion",
|
||||
"presence": "occupancy",
|
||||
"co_detected": "gas",
|
||||
"smoke_detected": "smoke"
|
||||
}
|
||||
|
||||
|
||||
@@ -29,17 +31,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink binary sensor platform."""
|
||||
import pywink
|
||||
|
||||
if discovery_info is None:
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if token is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing wink access_token. "
|
||||
"Get one at https://winkbearertoken.appspot.com/")
|
||||
return
|
||||
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
for sensor in pywink.get_sensors():
|
||||
if sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
@@ -47,6 +38,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for key in pywink.get_keys():
|
||||
add_devices([WinkBinarySensorDevice(key)])
|
||||
|
||||
for sensor in pywink.get_smoke_and_co_detectors():
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
@@ -70,15 +64,25 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
if self.capability == "loudness":
|
||||
return self.wink.loudness_boolean()
|
||||
state = self.wink.loudness_boolean()
|
||||
elif self.capability == "vibration":
|
||||
return self.wink.vibration_boolean()
|
||||
state = self.wink.vibration_boolean()
|
||||
elif self.capability == "brightness":
|
||||
return self.wink.brightness_boolean()
|
||||
state = self.wink.brightness_boolean()
|
||||
elif self.capability == "liquid_detected":
|
||||
return self.wink.liquid_boolean()
|
||||
state = self.wink.liquid_boolean()
|
||||
elif self.capability == "motion":
|
||||
state = self.wink.motion_boolean()
|
||||
elif self.capability == "presence":
|
||||
state = self.wink.presence_boolean()
|
||||
elif self.capability == "co_detected":
|
||||
state = self.wink.co_detected_boolean()
|
||||
elif self.capability == "smoke_detected":
|
||||
state = self.wink.smoke_detected_boolean()
|
||||
else:
|
||||
return self.wink.state()
|
||||
state = self.wink.state()
|
||||
|
||||
return state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
|
||||
@@ -36,8 +36,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
|
||||
# Make sure that we have values for the key before converting to int
|
||||
@@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
])
|
||||
return
|
||||
|
||||
if value.command_class == zwave.COMMAND_CLASS_SENSOR_BINARY:
|
||||
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class BloomSky(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def refresh_devices(self):
|
||||
"""Use the API to retreive a list of devices."""
|
||||
"""Use the API to retrieve a list of devices."""
|
||||
_LOGGER.debug("Fetching BloomSky update")
|
||||
response = requests.get(self.API_URL,
|
||||
headers={"Authorization": self._api_key},
|
||||
|
||||
@@ -9,30 +9,28 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.ffmpeg import (
|
||||
run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
REQUIREMENTS = ['ha-ffmpeg==0.10']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_INPUT = 'input'
|
||||
CONF_FFMPEG_BIN = 'ffmpeg_bin'
|
||||
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
|
||||
|
||||
DEFAULT_BINARY = 'ffmpeg'
|
||||
DEFAULT_NAME = 'FFmpeg'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_INPUT): cv.string,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
if not run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
add_devices([FFmpegCamera(config)])
|
||||
|
||||
|
||||
@@ -45,12 +43,11 @@ class FFmpegCamera(Camera):
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._input = config.get(CONF_INPUT)
|
||||
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
self._ffmpeg_bin = config.get(CONF_FFMPEG_BIN)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageSingle, IMAGE_JPEG
|
||||
ffmpeg = ImageSingle(self._ffmpeg_bin)
|
||||
ffmpeg = ImageSingle(get_binary())
|
||||
|
||||
return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments)
|
||||
@@ -59,7 +56,7 @@ class FFmpegCamera(Camera):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
stream = CameraMjpeg(self._ffmpeg_bin)
|
||||
stream = CameraMjpeg(get_binary())
|
||||
stream.open_camera(self._input, extra_cmd=self._extra_arguments)
|
||||
return response(
|
||||
stream,
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,7 +25,7 @@ CONF_STILL_IMAGE_URL = 'still_image_url'
|
||||
DEFAULT_NAME = 'Generic Camera'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_STILL_IMAGE_URL): vol.Any(cv.url, cv.template),
|
||||
vol.Required(CONF_STILL_IMAGE_URL): cv.template,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
|
||||
vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean,
|
||||
@@ -38,18 +38,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup a generic IP Camera."""
|
||||
add_devices([GenericCamera(config)])
|
||||
add_devices([GenericCamera(hass, config)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GenericCamera(Camera):
|
||||
"""A generic implementation of an IP camera."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
def __init__(self, hass, device_info):
|
||||
"""Initialize a generic camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
||||
self._still_image_url.hass = hass
|
||||
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
|
||||
|
||||
username = device_info.get(CONF_USERNAME)
|
||||
@@ -69,7 +71,7 @@ class GenericCamera(Camera):
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
try:
|
||||
url = template.render(self.hass, self._still_image_url)
|
||||
url = self._still_image_url.render()
|
||||
except TemplateError as err:
|
||||
_LOGGER.error('Error parsing template %s: %s',
|
||||
self._still_image_url, err)
|
||||
|
||||
@@ -36,10 +36,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup access to Netatmo Welcome cameras."""
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME)
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != []:
|
||||
if camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
add_devices([WelcomeCamera(data, camera_name, home)])
|
||||
@@ -49,7 +53,7 @@ class WelcomeCamera(Camera):
|
||||
"""Representation of the images published from Welcome camera."""
|
||||
|
||||
def __init__(self, data, camera_name, home):
|
||||
"""Setup for access to the BloomSky camera images."""
|
||||
"""Setup for access to the Netatmo camera images."""
|
||||
super(WelcomeCamera, self).__init__()
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
|
||||
@@ -8,28 +8,33 @@ import logging
|
||||
import socket
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.9.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NVR = 'nvr'
|
||||
CONF_KEY = 'key'
|
||||
|
||||
DEFAULT_PORT = 7080
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NVR): cv.string,
|
||||
vol.Required(CONF_KEY): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Discover cameras on a Unifi NVR."""
|
||||
if not validate_config({DOMAIN: config}, {DOMAIN: ['nvr', 'key']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
addr = config.get('nvr')
|
||||
key = config.get('key')
|
||||
try:
|
||||
port = int(config.get('port', 7080))
|
||||
except ValueError:
|
||||
_LOGGER.error('Invalid port number provided')
|
||||
return False
|
||||
addr = config[CONF_NVR]
|
||||
key = config[CONF_KEY]
|
||||
port = config[CONF_PORT]
|
||||
|
||||
from uvcclient import nvr
|
||||
nvrconn = nvr.UVCRemote(addr, port, key)
|
||||
|
||||
@@ -58,6 +58,12 @@ ATTR_OPERATION_LIST = "operation_list"
|
||||
ATTR_SWING_MODE = "swing_mode"
|
||||
ATTR_SWING_LIST = "swing_list"
|
||||
|
||||
CONVERTIBLE_ATTRIBUTE = [
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SET_AWAY_MODE_SCHEMA = vol.Schema({
|
||||
@@ -73,6 +79,7 @@ SET_TEMPERATURE_SCHEMA = vol.Schema({
|
||||
vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float),
|
||||
vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float),
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_OPERATION_MODE): cv.string,
|
||||
})
|
||||
SET_FAN_MODE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
@@ -116,8 +123,10 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def set_temperature(hass, temperature=None, entity_id=None,
|
||||
target_temp_high=None, target_temp_low=None):
|
||||
target_temp_high=None, target_temp_low=None,
|
||||
operation_mode=None):
|
||||
"""Set new target temperature."""
|
||||
kwargs = {
|
||||
key: value for key, value in [
|
||||
@@ -125,6 +134,7 @@ def set_temperature(hass, temperature=None, entity_id=None,
|
||||
(ATTR_TARGET_TEMP_HIGH, target_temp_high),
|
||||
(ATTR_TARGET_TEMP_LOW, target_temp_low),
|
||||
(ATTR_ENTITY_ID, entity_id),
|
||||
(ATTR_OPERATION_MODE, operation_mode)
|
||||
] if value is not None
|
||||
}
|
||||
_LOGGER.debug("set_temperature start data=%s", kwargs)
|
||||
@@ -235,10 +245,20 @@ def setup(hass, config):
|
||||
def temperature_set_service(service):
|
||||
"""Set temperature on the target climate devices."""
|
||||
target_climate = component.extract_from_service(service)
|
||||
kwargs = service.data
|
||||
for climate in target_climate:
|
||||
climate.set_temperature(**kwargs)
|
||||
|
||||
for climate in target_climate:
|
||||
kwargs = {}
|
||||
for value, temp in service.data.items():
|
||||
if value in CONVERTIBLE_ATTRIBUTE:
|
||||
kwargs[value] = convert_temperature(
|
||||
temp,
|
||||
hass.config.units.temperature_unit,
|
||||
climate.unit_of_measurement
|
||||
)
|
||||
else:
|
||||
kwargs[value] = temp
|
||||
|
||||
climate.set_temperature(**kwargs)
|
||||
if climate.should_poll:
|
||||
climate.update_ha_state(True)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
None, None, "Auto", "heat", None, None, None),
|
||||
DemoClimate("Hvac", 21, TEMP_CELSIUS, True, 22, "On High",
|
||||
67, 54, "Off", "cool", False, None, None),
|
||||
DemoClimate("Ecobee", 23, TEMP_CELSIUS, None, 23, "Auto Low",
|
||||
DemoClimate("Ecobee", None, TEMP_CELSIUS, None, 23, "Auto Low",
|
||||
None, None, "Auto", "auto", None, 24, 21)
|
||||
])
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.climate import (
|
||||
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -86,10 +86,16 @@ class Thermostat(ClimateDevice):
|
||||
self.hold_temp = hold_temp
|
||||
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
|
||||
'heat', 'off']
|
||||
self.update_without_throttle = False
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state from the thermostat."""
|
||||
self.data.update()
|
||||
if self.update_without_throttle:
|
||||
self.data.update(no_throttle=True)
|
||||
self.update_without_throttle = False
|
||||
else:
|
||||
self.data.update()
|
||||
|
||||
self.thermostat = self.data.ecobee.get_thermostat(
|
||||
self.thermostat_index)
|
||||
|
||||
@@ -101,25 +107,16 @@ class Thermostat(ClimateDevice):
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
if self.thermostat['settings']['useCelsius']:
|
||||
return TEMP_CELSIUS
|
||||
else:
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.thermostat['runtime']['actualTemperature'] / 10
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if (self.operation_mode == 'heat' or
|
||||
self.operation_mode == 'auxHeatOnly'):
|
||||
return self.target_temperature_low
|
||||
elif self.operation_mode == 'cool':
|
||||
return self.target_temperature_high
|
||||
else:
|
||||
return (self.target_temperature_low +
|
||||
self.target_temperature_high) / 2
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
@@ -214,37 +211,46 @@ class Thermostat(ClimateDevice):
|
||||
"away", "indefinite")
|
||||
else:
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index, "away")
|
||||
self.update_without_throttle = True
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
low_temp = temperature - 1
|
||||
high_temp = temperature + 1
|
||||
if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \
|
||||
kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None:
|
||||
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
high_temp = int(kwargs.get(ATTR_TARGET_TEMP_LOW))
|
||||
low_temp = int(kwargs.get(ATTR_TARGET_TEMP_HIGH))
|
||||
|
||||
if self.hold_temp:
|
||||
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
|
||||
high_temp, "indefinite")
|
||||
_LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, "
|
||||
"high=%s, is=%s", low_temp, isinstance(
|
||||
low_temp, (int, float)), high_temp,
|
||||
isinstance(high_temp, (int, float)))
|
||||
else:
|
||||
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
|
||||
high_temp)
|
||||
_LOGGER.debug("Setting ecobee temp to: low=%s, is=%s, "
|
||||
"high=%s, is=%s", low_temp, isinstance(
|
||||
low_temp, (int, float)), high_temp,
|
||||
isinstance(high_temp, (int, float)))
|
||||
self.update_without_throttle = True
|
||||
|
||||
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)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_fan_min_on_time(self, fan_min_on_time):
|
||||
"""Set the minimum fan on time."""
|
||||
self.data.ecobee.set_fan_min_on_time(self.thermostat_index,
|
||||
fan_min_on_time)
|
||||
self.update_without_throttle = True
|
||||
|
||||
# Home and Sleep mode aren't used in UI yet:
|
||||
|
||||
|
||||
@@ -5,16 +5,19 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.generic_thermostat/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import switch
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice)
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['switch', 'sensor']
|
||||
|
||||
@@ -30,18 +33,16 @@ CONF_TARGET_TEMP = 'target_temp'
|
||||
CONF_AC_MODE = 'ac_mode'
|
||||
CONF_MIN_DUR = 'min_cycle_duration'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required("platform"): "generic_thermostat",
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HEATER): cv.entity_id,
|
||||
vol.Required(CONF_SENSOR): cv.entity_id,
|
||||
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_AC_MODE): cv.boolean,
|
||||
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_AC_MODE): vol.Coerce(bool),
|
||||
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
@@ -56,10 +57,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
ac_mode = config.get(CONF_AC_MODE)
|
||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
|
||||
add_devices([GenericThermostat(hass, name, heater_entity_id,
|
||||
sensor_entity_id, min_temp,
|
||||
max_temp, target_temp, ac_mode,
|
||||
min_cycle_duration)])
|
||||
add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
@@ -110,7 +110,7 @@ class GenericThermostat(ClimateDevice):
|
||||
return self._cur_temp
|
||||
|
||||
@property
|
||||
def operation(self):
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self.ac_mode:
|
||||
cooling = self._active and self._is_device_active
|
||||
|
||||
@@ -99,6 +99,9 @@ class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||
return None
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return self._hmdevice.actionNodeData('MANU_MODE', temperature)
|
||||
self._hmdevice.set_temperature(temperature)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
|
||||
@@ -7,44 +7,67 @@ https://home-assistant.io/components/climate.honeywell/
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['evohomeclient==0.2.5',
|
||||
'somecomfort==0.2.1']
|
||||
'somecomfort==0.3.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AWAY_TEMP = "away_temperature"
|
||||
DEFAULT_AWAY_TEMP = 16
|
||||
ATTR_FAN = 'fan'
|
||||
ATTR_FANMODE = 'fanmode'
|
||||
ATTR_SYSTEM_MODE = 'system_mode'
|
||||
|
||||
CONF_AWAY_TEMPERATURE = 'away_temperature'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE = 16
|
||||
DEFAULT_REGION = 'eu'
|
||||
REGIONS = ['eu', 'us']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE, default=DEFAULT_AWAY_TEMPERATURE):
|
||||
vol.Coerce(float),
|
||||
vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the HoneywelL thermostat."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
region = config.get(CONF_REGION)
|
||||
|
||||
if region == 'us':
|
||||
return _setup_us(username, password, config, add_devices)
|
||||
else:
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
|
||||
def _setup_round(username, password, config, add_devices):
|
||||
"""Setup rounding function."""
|
||||
from evohomeclient import EvohomeClient
|
||||
|
||||
try:
|
||||
away_temp = float(config.get(CONF_AWAY_TEMP, DEFAULT_AWAY_TEMP))
|
||||
except ValueError:
|
||||
_LOGGER.error("value entered for item %s should convert to a number",
|
||||
CONF_AWAY_TEMP)
|
||||
return False
|
||||
|
||||
away_temp = config.get(CONF_AWAY_TEMPERATURE)
|
||||
evo_api = EvohomeClient(username, password)
|
||||
|
||||
try:
|
||||
zones = evo_api.temperatures(force_refresh=True)
|
||||
for i, zone in enumerate(zones):
|
||||
add_devices([RoundThermostat(evo_api,
|
||||
zone['id'],
|
||||
i == 0,
|
||||
away_temp)])
|
||||
add_devices(
|
||||
[RoundThermostat(evo_api, zone['id'], i == 0, away_temp)]
|
||||
)
|
||||
except socket.error:
|
||||
_LOGGER.error(
|
||||
"Connection error logging into the honeywell evohome web service"
|
||||
)
|
||||
"Connection error logging into the honeywell evohome web service")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -74,26 +97,6 @@ def _setup_us(username, password, config, add_devices):
|
||||
return True
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the honeywel thermostat."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
region = config.get('region', 'eu').lower()
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error("Missing required configuration items %s or %s",
|
||||
CONF_USERNAME, CONF_PASSWORD)
|
||||
return False
|
||||
if region not in ('us', 'eu'):
|
||||
_LOGGER.error('Region `%s` is invalid (use either us or eu)', region)
|
||||
return False
|
||||
|
||||
if region == 'us':
|
||||
return _setup_us(username, password, config, add_devices)
|
||||
else:
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
|
||||
class RoundThermostat(ClimateDevice):
|
||||
"""Representation of a Honeywell Round Connected thermostat."""
|
||||
|
||||
@@ -103,7 +106,7 @@ class RoundThermostat(ClimateDevice):
|
||||
self.device = device
|
||||
self._current_temperature = None
|
||||
self._target_temperature = None
|
||||
self._name = "round connected"
|
||||
self._name = 'round connected'
|
||||
self._id = zone_id
|
||||
self._master = master
|
||||
self._is_dhw = False
|
||||
@@ -143,7 +146,7 @@ class RoundThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
"""Get the current operation of the system."""
|
||||
return getattr(self.device, 'system_mode', None)
|
||||
return getattr(self.device, ATTR_SYSTEM_MODE, None)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
@@ -152,7 +155,7 @@ class RoundThermostat(ClimateDevice):
|
||||
|
||||
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
|
||||
"""Set the HVAC mode for the thermostat."""
|
||||
if hasattr(self.device, 'system_mode'):
|
||||
if hasattr(self.device, ATTR_SYSTEM_MODE):
|
||||
self.device.system_mode = operation_mode
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
@@ -186,8 +189,8 @@ class RoundThermostat(ClimateDevice):
|
||||
|
||||
self._current_temperature = data['temp']
|
||||
self._target_temperature = data['setpoint']
|
||||
if data['thermostat'] == "DOMESTIC_HOT_WATER":
|
||||
self._name = "Hot Water"
|
||||
if data['thermostat'] == 'DOMESTIC_HOT_WATER':
|
||||
self._name = 'Hot Water'
|
||||
self._is_dhw = True
|
||||
else:
|
||||
self._name = data['name']
|
||||
@@ -236,7 +239,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return getattr(self._device, 'system_mode', None)
|
||||
return getattr(self._device, ATTR_SYSTEM_MODE, None)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set target temperature."""
|
||||
@@ -255,9 +258,11 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {'fan': (self.is_fan_on and 'running' or 'idle'),
|
||||
'fanmode': self._device.fan_mode,
|
||||
'system_mode': self._device.system_mode}
|
||||
return {
|
||||
ATTR_FAN: (self.is_fan_on and 'running' or 'idle'),
|
||||
ATTR_FANMODE: self._device.fan_mode,
|
||||
ATTR_SYSTEM_MODE: self._device.system_mode,
|
||||
}
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
@@ -269,5 +274,5 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
|
||||
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
|
||||
"""Set the system mode (Cool, Heat, etc)."""
|
||||
if hasattr(self._device, 'system_mode'):
|
||||
if hasattr(self._device, ATTR_SYSTEM_MODE):
|
||||
self._device.system_mode = operation_mode
|
||||
|
||||
@@ -2,26 +2,37 @@
|
||||
Support for KNX thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/knx/
|
||||
https://home-assistant.io/components/climate.knx/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.knx import (
|
||||
KNXConfig, KNXMultiAddressDevice)
|
||||
|
||||
DEPENDENCIES = ["knx"]
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
|
||||
from homeassistant.const import (CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ADDRESS = 'address'
|
||||
CONF_SETPOINT_ADDRESS = 'setpoint_address'
|
||||
CONF_TEMPERATURE_ADDRESS = 'temperature_address'
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
DEFAULT_NAME = 'KNX Thermostat'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADDRESS): cv.string,
|
||||
vol.Required(CONF_SETPOINT_ADDRESS): cv.string,
|
||||
vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
add_entities([
|
||||
KNXThermostat(hass, KNXConfig(config))
|
||||
])
|
||||
add_devices([KNXThermostat(hass, KNXConfig(config))])
|
||||
|
||||
|
||||
class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
@@ -39,9 +50,8 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the thermostat based on the given configuration."""
|
||||
KNXMultiAddressDevice.__init__(self, hass, config,
|
||||
["temperature", "setpoint"],
|
||||
["mode"])
|
||||
KNXMultiAddressDevice.__init__(
|
||||
self, hass, config, ['temperature', 'setpoint'], ['mode'])
|
||||
|
||||
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
|
||||
self._away = False # not yet supported
|
||||
@@ -62,14 +72,14 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
"""Return the current temperature."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
return knx2_to_float(self.value("temperature"))
|
||||
return knx2_to_float(self.value('temperature'))
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
return knx2_to_float(self.value("setpoint"))
|
||||
return knx2_to_float(self.value('setpoint'))
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -78,7 +88,7 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
return
|
||||
from knxip.conversion import float_to_knx2
|
||||
|
||||
self.set_value("setpoint", float_to_knx2(temperature))
|
||||
self.set_value('setpoint', float_to_knx2(temperature))
|
||||
_LOGGER.debug("Set target temperature to %s", temperature)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
mysensors platform that offers a Climate(MySensors-HVAC) component.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/climate.mysensors
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.climate import (
|
||||
STATE_COOL, STATE_HEAT, STATE_OFF, STATE_AUTO, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW)
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DICT_HA_TO_MYS = {STATE_COOL: "CoolOn", STATE_HEAT: "HeatOn",
|
||||
STATE_AUTO: "AutoChangeOver", STATE_OFF: "Off"}
|
||||
DICT_MYS_TO_HA = {"CoolOn": STATE_COOL, "HeatOn": STATE_HEAT,
|
||||
"AutoChangeOver": STATE_AUTO, "Off": STATE_OFF}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors climate."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
if float(gateway.protocol_version) < 1.5:
|
||||
continue
|
||||
pres = gateway.const.Presentation
|
||||
set_req = gateway.const.SetReq
|
||||
map_sv_types = {
|
||||
pres.S_HVAC: [set_req.V_HVAC_FLOW_STATE],
|
||||
}
|
||||
devices = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
map_sv_types, devices, add_devices, MySensorsHVAC))
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-public-methods
|
||||
class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
"""Representation of a MySensorsHVAC hvac."""
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return True if unable to access real state of entity."""
|
||||
return self.gateway.optimistic
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return (TEMP_CELSIUS
|
||||
if self.gateway.metric else TEMP_FAHRENHEIT)
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._values.get(self.gateway.const.SetReq.V_TEMP)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_HVAC_SETPOINT_COOL in self._values and \
|
||||
set_req.V_HVAC_SETPOINT_HEAT in self._values:
|
||||
return None
|
||||
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
|
||||
if temp is None:
|
||||
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
|
||||
return temp
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_HVAC_SETPOINT_HEAT in self._values:
|
||||
return self._values.get(set_req.V_HVAC_SETPOINT_COOL)
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lowbound target temperature we try to reach."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_HVAC_SETPOINT_COOL in self._values:
|
||||
return self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._values.get(self.gateway.const.SetReq.V_HVAC_FLOW_STATE)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT]
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._values.get(self.gateway.const.SetReq.V_HVAC_SPEED)
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return ["Auto", "Min", "Normal", "Max"]
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
heat = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
|
||||
cool = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
|
||||
updates = ()
|
||||
if temp is not None:
|
||||
if heat is not None:
|
||||
# Set HEAT Target temperature
|
||||
value_type = set_req.V_HVAC_SETPOINT_HEAT
|
||||
elif cool is not None:
|
||||
# Set COOL Target temperature
|
||||
value_type = set_req.V_HVAC_SETPOINT_COOL
|
||||
if heat is not None or cool is not None:
|
||||
updates = [(value_type, temp)]
|
||||
elif all(val is not None for val in (low, high, heat, cool)):
|
||||
updates = [
|
||||
(set_req.V_HVAC_SETPOINT_HEAT, low),
|
||||
(set_req.V_HVAC_SETPOINT_COOL, high)]
|
||||
for value_type, value in updates:
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, value_type, value)
|
||||
if self.gateway.optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._values[value_type] = value
|
||||
self.update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target temperature."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(self.node_id, self.child_id,
|
||||
set_req.V_HVAC_SPEED, fan)
|
||||
if self.gateway.optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._values[set_req.V_HVAC_SPEED] = fan
|
||||
self.update_ha_state()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target temperature."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(self.node_id, self.child_id,
|
||||
set_req.V_HVAC_FLOW_STATE,
|
||||
DICT_HA_TO_MYS[operation_mode])
|
||||
if self.gateway.optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode
|
||||
self.update_ha_state()
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
child = node.children[self.child_id]
|
||||
for value_type, value in child.values.items():
|
||||
_LOGGER.debug(
|
||||
'%s: value_type %s, value = %s', self._name, value_type, value)
|
||||
if value_type == set_req.V_HVAC_FLOW_STATE:
|
||||
self._values[value_type] = DICT_MYS_TO_HA[value]
|
||||
else:
|
||||
self._values[value_type] = value
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set new target humidity."""
|
||||
_LOGGER.error("Service Not Implemented yet")
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
_LOGGER.error("Service Not Implemented yet")
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
_LOGGER.error("Service Not Implemented yet")
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
_LOGGER.error("Service Not Implemented yet")
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
_LOGGER.error("Service Not Implemented yet")
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
_LOGGER.error("Service Not Implemented yet")
|
||||
@@ -4,15 +4,18 @@ Support for Nest thermostats.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.nest/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.nest as nest
|
||||
from homeassistant.components.climate import (
|
||||
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, CONF_SCAN_INTERVAL, ATTR_TEMPERATURE)
|
||||
TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_INTERVAL):
|
||||
@@ -22,18 +25,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Nest thermostat."""
|
||||
add_devices([NestThermostat(structure, device)
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
add_devices([NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in nest.devices()])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=abstract-method,too-many-public-methods
|
||||
class NestThermostat(ClimateDevice):
|
||||
"""Representation of a Nest thermostat."""
|
||||
|
||||
def __init__(self, structure, device):
|
||||
def __init__(self, structure, device, temp_unit):
|
||||
"""Initialize the thermostat."""
|
||||
self._unit = temp_unit
|
||||
self.structure = structure
|
||||
self.device = device
|
||||
self._fan_list = [STATE_ON, STATE_AUTO]
|
||||
self._operation_list = [STATE_HEAT, STATE_COOL, STATE_AUTO,
|
||||
STATE_OFF]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -60,7 +68,6 @@ class NestThermostat(ClimateDevice):
|
||||
return {
|
||||
"humidity": self.device.humidity,
|
||||
"target_humidity": self.device.target_humidity,
|
||||
"mode": self.device.mode
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -69,43 +76,26 @@ class NestThermostat(ClimateDevice):
|
||||
return self.device.temperature
|
||||
|
||||
@property
|
||||
def operation(self):
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self.device.hvac_ac_state is True:
|
||||
if self.device.mode == 'cool':
|
||||
return STATE_COOL
|
||||
elif self.device.hvac_heater_state is True:
|
||||
elif self.device.mode == 'heat':
|
||||
return STATE_HEAT
|
||||
elif self.device.mode == 'range':
|
||||
return STATE_AUTO
|
||||
elif self.device.mode == 'off':
|
||||
return STATE_OFF
|
||||
else:
|
||||
return STATE_IDLE
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.device.mode == 'range':
|
||||
low, high = self.target_temperature_low, \
|
||||
self.target_temperature_high
|
||||
if self.operation == STATE_COOL:
|
||||
temp = high
|
||||
elif self.operation == STATE_HEAT:
|
||||
temp = low
|
||||
else:
|
||||
# If the outside temp is lower than the current temp, consider
|
||||
# the 'low' temp to the target, otherwise use the high temp
|
||||
if (self.device.structure.weather.current.temperature <
|
||||
self.current_temperature):
|
||||
temp = low
|
||||
else:
|
||||
temp = high
|
||||
if self.device.mode != 'range' and not self.is_away_mode_on:
|
||||
return self.device.target
|
||||
else:
|
||||
if self.is_away_mode_on:
|
||||
# away_temperature is a low, high tuple. Only one should be set
|
||||
# if not in range mode, the other will be None
|
||||
temp = self.device.away_temperature[0] or \
|
||||
self.device.away_temperature[1]
|
||||
else:
|
||||
temp = self.device.target
|
||||
|
||||
return temp
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@@ -115,7 +105,8 @@ class NestThermostat(ClimateDevice):
|
||||
return self.device.away_temperature[0]
|
||||
if self.device.mode == 'range':
|
||||
return self.device.target[0]
|
||||
return self.target_temperature
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
@@ -125,7 +116,8 @@ class NestThermostat(ClimateDevice):
|
||||
return self.device.away_temperature[1]
|
||||
if self.device.mode == 'range':
|
||||
return self.device.target[1]
|
||||
return self.target_temperature
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
@@ -134,19 +126,32 @@ class NestThermostat(ClimateDevice):
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
if self.device.mode == 'range':
|
||||
if self.target_temperature == self.target_temperature_low:
|
||||
temperature = (temperature, self.target_temperature_high)
|
||||
elif self.target_temperature == self.target_temperature_high:
|
||||
temperature = (self.target_temperature_low, temperature)
|
||||
self.device.target = temperature
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
|
||||
if self.device.mode == 'range':
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
self.device.target = temp
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
self.device.mode = operation_mode
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.device.mode = 'heat'
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.device.mode = 'cool'
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.device.mode = 'range'
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.device.mode = 'off'
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
@@ -157,17 +162,18 @@ class NestThermostat(ClimateDevice):
|
||||
self.structure.away = False
|
||||
|
||||
@property
|
||||
def is_fan_on(self):
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
return self.device.fan
|
||||
return STATE_ON if self.device.fan else STATE_AUTO
|
||||
|
||||
def turn_fan_on(self):
|
||||
"""Turn fan on."""
|
||||
self.device.fan = True
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
def turn_fan_off(self):
|
||||
"""Turn fan off."""
|
||||
self.device.fan = False
|
||||
def set_fan_mode(self, fan):
|
||||
"""Turn fan on/off."""
|
||||
self.device.fan = fan.lower()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
||||
@@ -4,13 +4,24 @@ Support for Proliphix NT10e Thermostats.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.proliphix/
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
|
||||
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['proliphix==0.3.1']
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Proliphix thermostats."""
|
||||
@@ -22,9 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
pdp = proliphix.PDP(host, username, password)
|
||||
|
||||
add_devices([
|
||||
ProliphixThermostat(pdp)
|
||||
])
|
||||
add_devices([ProliphixThermostat(pdp)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
@@ -56,7 +65,7 @@ class ProliphixThermostat(ClimateDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
"fan": self._pdp.fan_state
|
||||
ATTR_FAN: self._pdp.fan_state
|
||||
}
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,15 +8,28 @@ import datetime
|
||||
import logging
|
||||
from urllib.error import URLError
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF,
|
||||
ClimateDevice)
|
||||
ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['radiotherm==1.2']
|
||||
HOLD_TEMP = 'hold_temp'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
ATTR_MODE = 'mode'
|
||||
|
||||
CONF_HOLD_TEMP = 'hold_temp'
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Radio Thermostat."""
|
||||
@@ -29,10 +42,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hosts.append(radiotherm.discover.discover_address())
|
||||
|
||||
if hosts is None:
|
||||
_LOGGER.error("No radiotherm thermostats detected.")
|
||||
_LOGGER.error("No Radiotherm Thermostats detected")
|
||||
return False
|
||||
|
||||
hold_temp = config.get(HOLD_TEMP, False)
|
||||
hold_temp = config.get(CONF_HOLD_TEMP)
|
||||
tstats = []
|
||||
|
||||
for host in hosts:
|
||||
@@ -60,6 +73,7 @@ class RadioThermostat(ClimateDevice):
|
||||
self._name = None
|
||||
self.hold_temp = hold_temp
|
||||
self.update()
|
||||
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -75,8 +89,8 @@ class RadioThermostat(ClimateDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
"fan": self.device.fmode['human'],
|
||||
"mode": self.device.tmode['human']
|
||||
ATTR_FAN: self.device.fmode['human'],
|
||||
ATTR_MODE: self.device.tmode['human']
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -89,6 +103,11 @@ class RadioThermostat(ClimateDevice):
|
||||
"""Return the current operation. head, cool idle."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the operation modes list."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
@@ -124,8 +143,11 @@ class RadioThermostat(ClimateDevice):
|
||||
def set_time(self):
|
||||
"""Set device time."""
|
||||
now = datetime.datetime.now()
|
||||
self.device.time = {'day': now.weekday(),
|
||||
'hour': now.hour, 'minute': now.minute}
|
||||
self.device.time = {
|
||||
'day': now.weekday(),
|
||||
'hour': now.hour,
|
||||
'minute': now.minute
|
||||
}
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode (auto, cool, heat, off)."""
|
||||
|
||||
@@ -34,6 +34,18 @@ set_temperature:
|
||||
description: New target temperature for hvac
|
||||
example: 25
|
||||
|
||||
target_temp_high:
|
||||
description: New target high tempereature for hvac
|
||||
example: 26
|
||||
|
||||
target_temp_low:
|
||||
description: New target low temperature for hvac
|
||||
example: 20
|
||||
|
||||
operation_mode:
|
||||
description: Operation mode to set temperature to. This defaults to current_operation mode if not set, or set incorrectly.
|
||||
example: 'Heat'
|
||||
|
||||
set_humidity:
|
||||
description: Set target humidity of climate device
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
Support for Vera thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE
|
||||
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
OPERATION_LIST = ["Heat", "Cool", "Auto Changeover", "Off"]
|
||||
FAN_OPERATION_LIST = ["On", "Auto", "Cycle"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Find and return Vera thermostats."""
|
||||
add_devices_callback(
|
||||
VeraThermostat(device, VERA_CONTROLLER) for
|
||||
device in VERA_DEVICES['climate'])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
"""Representation of a Vera Thermostat."""
|
||||
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
mode = self.vera_device.get_hvac_mode()
|
||||
if mode == "HeatOn":
|
||||
return OPERATION_LIST[0] # heat
|
||||
elif mode == "CoolOn":
|
||||
return OPERATION_LIST[1] # cool
|
||||
elif mode == "AutoChangeOver":
|
||||
return OPERATION_LIST[2] # auto
|
||||
elif mode == "Off":
|
||||
return OPERATION_LIST[3] # off
|
||||
return "Off"
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
mode = self.vera_device.get_fan_mode()
|
||||
if mode == "ContinuousOn":
|
||||
return FAN_OPERATION_LIST[0] # on
|
||||
elif mode == "Auto":
|
||||
return FAN_OPERATION_LIST[1] # auto
|
||||
elif mode == "PeriodicOn":
|
||||
return FAN_OPERATION_LIST[2] # cycle
|
||||
return "Auto"
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return FAN_OPERATION_LIST
|
||||
|
||||
def set_fan_mode(self, mode):
|
||||
"""Set new target temperature."""
|
||||
if mode == FAN_OPERATION_LIST[0]:
|
||||
self.vera_device.fan_on()
|
||||
elif mode == FAN_OPERATION_LIST[1]:
|
||||
self.vera_device.fan_auto()
|
||||
elif mode == FAN_OPERATION_LIST[2]:
|
||||
return self.vera_device.fan_cycle()
|
||||
|
||||
@property
|
||||
def current_power_mwh(self):
|
||||
"""Current power usage in mWh."""
|
||||
power = self.vera_device.power
|
||||
if power:
|
||||
return convert(power, float, 0.0) * 1000
|
||||
|
||||
def update(self):
|
||||
"""Called by the vera device callback to update state."""
|
||||
self._state = self.vera_device.get_hvac_mode()
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.vera_device.get_current_temperature()
|
||||
|
||||
@property
|
||||
def operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self.vera_device.get_hvac_state()
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.vera_device.get_current_goal_temperature()
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
self.vera_device.set_temperature(kwargs.get(ATTR_TEMPERATURE))
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set HVAC mode (auto, cool, heat, off)."""
|
||||
if operation_mode == OPERATION_LIST[3]: # off
|
||||
self.vera_device.turn_off()
|
||||
elif operation_mode == OPERATION_LIST[2]: # auto
|
||||
self.vera_device.turn_auto_on()
|
||||
elif operation_mode == OPERATION_LIST[1]: # cool
|
||||
self.vera_device.turn_cool_on()
|
||||
elif operation_mode == OPERATION_LIST[0]: # heat
|
||||
self.vera_device.turn_heat_on()
|
||||
|
||||
def turn_fan_on(self):
|
||||
"""Turn fan on."""
|
||||
self.vera_device.fan_on()
|
||||
|
||||
def turn_fan_off(self):
|
||||
"""Turn fan off."""
|
||||
self.vera_device.fan_auto()
|
||||
@@ -8,9 +8,9 @@ https://home-assistant.io/components/climate.zwave/
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import (
|
||||
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ATTR_OPERATION_MODE)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
@@ -28,12 +28,6 @@ HORSTMANN = 0x0059
|
||||
HORSTMANN_HRT4_ZW = 0x3
|
||||
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
|
||||
|
||||
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
|
||||
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
|
||||
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
|
||||
COMMAND_CLASS_CONFIGURATION = 0x70
|
||||
|
||||
WORKAROUND_ZXT_120 = 'zxt_120'
|
||||
WORKAROUND_HRT4_ZW = 'hrt4_zw'
|
||||
|
||||
@@ -67,21 +61,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
discovery_info, zwave.NETWORK)
|
||||
return
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
if value.index != 1: # Only add 1 device
|
||||
return
|
||||
add_devices([ZWaveClimate(value, temp_unit)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, abstract-method
|
||||
# pylint: disable=abstract-method
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Represents a ZWave Climate device."""
|
||||
|
||||
# pylint: disable=too-many-public-methods, too-many-instance-attributes
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, value, temp_unit):
|
||||
"""Initialize the zwave climate device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
@@ -132,7 +124,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# Operation Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
self._current_operation = value.data
|
||||
self._index_operation = SET_TEMP_TO_INDEX.get(
|
||||
self._current_operation)
|
||||
@@ -141,14 +133,16 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("self._current_operation=%s",
|
||||
self._current_operation)
|
||||
# Current Temp
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
if value.label == 'Temperature':
|
||||
self._current_temperature = int(value.data)
|
||||
self._unit = value.units
|
||||
# Fan Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
self._current_fan_mode = value.data
|
||||
self._fan_list = list(value.data_items)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
@@ -156,17 +150,27 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
self._current_swing_mode = value.data
|
||||
self._swing_list = list(value.data_items)
|
||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = int(self._current_temperature)
|
||||
break
|
||||
if self.current_operation is not None and \
|
||||
self.current_operation != 'Off':
|
||||
if self._index_operation != value.index:
|
||||
@@ -234,16 +238,26 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
else:
|
||||
return
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
_LOGGER.debug("set_temperature operation_mode=%s", operation_mode)
|
||||
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if operation_mode is not None:
|
||||
setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode)
|
||||
if value.index != setpoint_mode:
|
||||
continue
|
||||
_LOGGER.debug("setpoint_mode=%s", setpoint_mode)
|
||||
value.data = temperature
|
||||
break
|
||||
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
if self.current_operation is not None:
|
||||
if self._hrt4_zw and self.current_operation == 'Off':
|
||||
# HRT4-ZW can change setpoint when off.
|
||||
@@ -263,6 +277,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._target_temperature = temperature
|
||||
# ZXT-120 responds only to whole int
|
||||
value.data = round(temperature, 0)
|
||||
self.update_ha_state()
|
||||
break
|
||||
else:
|
||||
_LOGGER.debug("Setting new setpoint for %s, "
|
||||
@@ -280,17 +295,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
if value.command_class == 68 and value.index == 0:
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE).
|
||||
values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE and \
|
||||
value.index == 0:
|
||||
value.data = bytes(fan, 'utf-8')
|
||||
break
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
if value.command_class == 64 and value.index == 0:
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_THERMOSTAT_MODE and value.index == 0:
|
||||
value.data = bytes(operation_mode, 'utf-8')
|
||||
break
|
||||
|
||||
@@ -298,7 +317,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Set new target swing mode."""
|
||||
if self._zxt_120 == 1:
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
value.data = bytes(swing_mode, 'utf-8')
|
||||
break
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.11.1']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.12.0']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
@@ -29,6 +29,10 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Register the process service."""
|
||||
@@ -48,8 +52,8 @@ def setup(hass, config):
|
||||
|
||||
name, command = match.groups()
|
||||
entities = {state.entity_id: state.name for state in hass.states.all()}
|
||||
entity_ids = fuzzyExtract.extractOne(name, entities,
|
||||
score_cutoff=65)[2]
|
||||
entity_ids = fuzzyExtract.extractOne(
|
||||
name, entities, score_cutoff=65)[2]
|
||||
|
||||
if not entity_ids:
|
||||
logger.error(
|
||||
@@ -70,6 +74,7 @@ def setup(hass, config):
|
||||
logger.error('Got unsupported command %s from text %s',
|
||||
command, text)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PROCESS, process,
|
||||
schema=SERVICE_PROCESS_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -25,9 +25,8 @@ from homeassistant.const import (
|
||||
DOMAIN = 'cover'
|
||||
SCAN_INTERVAL = 15
|
||||
|
||||
GROUP_NAME_ALL_COVERS = 'all_covers'
|
||||
ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format(
|
||||
GROUP_NAME_ALL_COVERS)
|
||||
GROUP_NAME_ALL_COVERS = 'all covers'
|
||||
ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.const import (
|
||||
CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, CONF_COMMAND_STATE,
|
||||
CONF_COMMAND_STOP, CONF_COVERS, CONF_VALUE_TEMPLATE, CONF_FRIENDLY_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,7 +23,7 @@ COVER_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_COMMAND_STATE): cv.string,
|
||||
vol.Optional(CONF_COMMAND_STOP, default='true'): cv.string,
|
||||
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE, default='{{ value }}'): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -38,6 +37,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
covers = []
|
||||
|
||||
for device_name, device_config in devices.items():
|
||||
value_template = device_config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
covers.append(
|
||||
CommandCover(
|
||||
hass,
|
||||
@@ -46,7 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
device_config.get(CONF_COMMAND_CLOSE),
|
||||
device_config.get(CONF_COMMAND_STOP),
|
||||
device_config.get(CONF_COMMAND_STATE),
|
||||
device_config.get(CONF_VALUE_TEMPLATE),
|
||||
value_template,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -136,8 +139,8 @@ class CommandCover(CoverDevice):
|
||||
if self._command_state:
|
||||
payload = str(self._query_state())
|
||||
if self._value_template:
|
||||
payload = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, payload)
|
||||
payload = self._value_template.render_with_possible_json_value(
|
||||
payload)
|
||||
self._state = int(payload)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Support for ISY994 covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.isy994/
|
||||
"""
|
||||
import logging
|
||||
from typing import Callable # noqa
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, DOMAIN
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
0: STATE_CLOSED,
|
||||
101: STATE_UNKNOWN,
|
||||
}
|
||||
|
||||
UOM = ['97']
|
||||
STATES = [STATE_OPEN, STATE_CLOSED, 'closing', 'opening', 'stopped']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Setup the ISY994 cover platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error('A connection has not been made to the ISY controller.')
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.NODES, units=UOM,
|
||||
states=STATES):
|
||||
devices.append(ISYCoverDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
actions = program[isy.KEY_ACTIONS]
|
||||
assert actions.dtype == 'program', 'Not a program'
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
devices.append(ISYCoverProgram(program.name, status, actions))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYCoverDevice(isy.ISYDevice, CoverDevice):
|
||||
"""Representation of an ISY994 cover device."""
|
||||
|
||||
def __init__(self, node: object):
|
||||
"""Initialize the ISY994 cover device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int:
|
||||
"""Get the current cover position."""
|
||||
return sorted((0, self.value, 100))[1]
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
"""Get whether the ISY994 cover device is closed."""
|
||||
return self.state == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 cover device."""
|
||||
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."""
|
||||
if not self._node.on(val=100):
|
||||
_LOGGER.error('Unable to open the cover')
|
||||
|
||||
def close_cover(self, **kwargs) -> None:
|
||||
"""Send the close cover command to the ISY994 cover device."""
|
||||
if not self._node.off():
|
||||
_LOGGER.error('Unable to close the cover')
|
||||
|
||||
|
||||
class ISYCoverProgram(ISYCoverDevice):
|
||||
"""Representation of an ISY994 cover program."""
|
||||
|
||||
def __init__(self, name: str, node: object, actions: object) -> None:
|
||||
"""Initialize the ISY994 cover program."""
|
||||
ISYCoverDevice.__init__(self, node)
|
||||
self._name = name
|
||||
self._actions = actions
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 cover program."""
|
||||
return STATE_CLOSED if bool(self.value) else STATE_OPEN
|
||||
|
||||
def open_cover(self, **kwargs) -> None:
|
||||
"""Send the open cover command to the ISY994 cover program."""
|
||||
if not self._actions.runThen():
|
||||
_LOGGER.error('Unable to open the cover')
|
||||
|
||||
def close_cover(self, **kwargs) -> None:
|
||||
"""Send the close cover command to the ISY994 cover program."""
|
||||
if not self._actions.runElse():
|
||||
_LOGGER.error('Unable to close the cover')
|
||||
@@ -15,7 +15,6 @@ from homeassistant.const import (
|
||||
STATE_CLOSED)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -28,10 +27,10 @@ CONF_PAYLOAD_STOP = 'payload_stop'
|
||||
CONF_STATE_OPEN = 'state_open'
|
||||
CONF_STATE_CLOSED = 'state_closed'
|
||||
|
||||
DEFAULT_NAME = "MQTT Cover"
|
||||
DEFAULT_PAYLOAD_OPEN = "OPEN"
|
||||
DEFAULT_PAYLOAD_CLOSE = "CLOSE"
|
||||
DEFAULT_PAYLOAD_STOP = "STOP"
|
||||
DEFAULT_NAME = 'MQTT Cover'
|
||||
DEFAULT_PAYLOAD_OPEN = 'OPEN'
|
||||
DEFAULT_PAYLOAD_CLOSE = 'CLOSE'
|
||||
DEFAULT_PAYLOAD_STOP = 'STOP'
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
DEFAULT_RETAIN = False
|
||||
|
||||
@@ -43,27 +42,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
|
||||
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Add MQTT Cover."""
|
||||
add_devices_callback([MqttCover(
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the MQTT Cover."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
add_devices([MqttCover(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config[CONF_COMMAND_TOPIC],
|
||||
config[CONF_QOS],
|
||||
config[CONF_RETAIN],
|
||||
config[CONF_STATE_OPEN],
|
||||
config[CONF_STATE_CLOSED],
|
||||
config[CONF_PAYLOAD_OPEN],
|
||||
config[CONF_PAYLOAD_CLOSE],
|
||||
config[CONF_PAYLOAD_STOP],
|
||||
config[CONF_OPTIMISTIC],
|
||||
config.get(CONF_VALUE_TEMPLATE)
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
config.get(CONF_STATE_OPEN),
|
||||
config.get(CONF_STATE_CLOSED),
|
||||
config.get(CONF_PAYLOAD_OPEN),
|
||||
config.get(CONF_PAYLOAD_CLOSE),
|
||||
config.get(CONF_PAYLOAD_STOP),
|
||||
config.get(CONF_OPTIMISTIC),
|
||||
value_template,
|
||||
)])
|
||||
|
||||
|
||||
@@ -93,8 +93,8 @@ class MqttCover(CoverDevice):
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = template.render_with_possible_json_value(
|
||||
hass, value_template, payload)
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._state_open:
|
||||
self._state = False
|
||||
_LOGGER.warning("state=%s", int(self._state))
|
||||
@@ -111,8 +111,8 @@ class MqttCover(CoverDevice):
|
||||
self.update_ha_state()
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Payload is not True or False or"
|
||||
" integer(0-100) %s", payload)
|
||||
"Payload is not True, False, or integer (0-100): %s",
|
||||
payload)
|
||||
if self._state_topic is None:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
@@ -149,7 +149,7 @@ class MqttCover(CoverDevice):
|
||||
self._qos, self._retain)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = 100
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
@@ -158,7 +158,7 @@ class MqttCover(CoverDevice):
|
||||
self._qos, self._retain)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = 0
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Support for MySensors covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.mysensors/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.cover import CoverDevice, ATTR_POSITION
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for covers."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
pres = gateway.const.Presentation
|
||||
set_req = gateway.const.SetReq
|
||||
map_sv_types = {
|
||||
pres.S_COVER: [set_req.V_DIMMER, set_req.V_LIGHT],
|
||||
}
|
||||
if float(gateway.protocol_version) >= 1.5:
|
||||
map_sv_types.update({
|
||||
pres.S_COVER: [set_req.V_PERCENTAGE, set_req.V_STATUS],
|
||||
})
|
||||
devices = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
map_sv_types, devices, add_devices, MySensorsCover))
|
||||
|
||||
|
||||
class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
||||
"""Representation of the value of a MySensors Cover child node."""
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return True if unable to access real state of entity."""
|
||||
return self.gateway.optimistic
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return True if cover is closed."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_DIMMER in self._values:
|
||||
return self._values.get(set_req.V_DIMMER) == 0
|
||||
else:
|
||||
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
set_req = self.gateway.const.SetReq
|
||||
return self._values.get(set_req.V_DIMMER)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, set_req.V_UP, 1)
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
if set_req.V_DIMMER in self._values:
|
||||
self._values[set_req.V_DIMMER] = 100
|
||||
else:
|
||||
self._values[set_req.V_LIGHT] = STATE_ON
|
||||
self.update_ha_state()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, set_req.V_DOWN, 1)
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
if set_req.V_DIMMER in self._values:
|
||||
self._values[set_req.V_DIMMER] = 0
|
||||
else:
|
||||
self._values[set_req.V_LIGHT] = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, set_req.V_DIMMER, position)
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._values[set_req.V_DIMMER] = position
|
||||
self.update_ha_state()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the device."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, set_req.V_STOP, 1)
|
||||
@@ -7,64 +7,69 @@ https://github.com/andrewshilliday/garage-door-controller
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.rpi_gpio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from time import sleep
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
RELAY_TIME = 'relay_time'
|
||||
STATE_PULL_MODE = 'state_pull_mode'
|
||||
DEFAULT_PULL_MODE = 'UP'
|
||||
DEFAULT_RELAY_TIME = .2
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_COVERS = 'covers'
|
||||
CONF_RELAY_PIN = 'relay_pin'
|
||||
CONF_RELAY_TIME = 'relay_time'
|
||||
CONF_STATE_PIN = 'state_pin'
|
||||
CONF_STATE_PULL_MODE = 'state_pull_mode'
|
||||
|
||||
DEFAULT_RELAY_TIME = .2
|
||||
DEFAULT_STATE_PULL_MODE = 'UP'
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
|
||||
_COVERS_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.Schema({
|
||||
'name': str,
|
||||
'relay_pin': int,
|
||||
'state_pin': int,
|
||||
CONF_NAME: cv.string,
|
||||
CONF_RELAY_PIN: cv.positive_int,
|
||||
CONF_STATE_PIN: cv.positive_int,
|
||||
})
|
||||
]
|
||||
)
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
'platform': str,
|
||||
vol.Required('covers'): _COVERS_SCHEMA,
|
||||
vol.Optional(STATE_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string,
|
||||
vol.Optional(RELAY_TIME, default=DEFAULT_RELAY_TIME): vol.Coerce(int),
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): _COVERS_SCHEMA,
|
||||
vol.Optional(CONF_STATE_PULL_MODE, default=DEFAULT_STATE_PULL_MODE):
|
||||
cv.string,
|
||||
vol.Optional(CONF_RELAY_TIME, default=DEFAULT_RELAY_TIME): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the cover platform."""
|
||||
relay_time = config.get(RELAY_TIME)
|
||||
state_pull_mode = config.get(STATE_PULL_MODE)
|
||||
"""Setup the RPi cover platform."""
|
||||
relay_time = config.get(CONF_RELAY_TIME)
|
||||
state_pull_mode = config.get(CONF_STATE_PULL_MODE)
|
||||
covers = []
|
||||
covers_conf = config.get('covers')
|
||||
covers_conf = config.get(CONF_COVERS)
|
||||
|
||||
for cover in covers_conf:
|
||||
covers.append(RPiGPIOCover(cover['name'], cover['relay_pin'],
|
||||
cover['state_pin'],
|
||||
state_pull_mode,
|
||||
relay_time))
|
||||
covers.append(RPiGPIOCover(
|
||||
cover[CONF_NAME], cover[CONF_RELAY_PIN], cover[CONF_STATE_PIN],
|
||||
state_pull_mode, relay_time))
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class RPiGPIOCover(CoverDevice):
|
||||
"""Representation of a Raspberry cover."""
|
||||
"""Representation of a Raspberry GPIO cover."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, relay_pin, state_pin,
|
||||
state_pull_mode, relay_time):
|
||||
def __init__(self, name, relay_pin, state_pin, state_pull_mode,
|
||||
relay_time):
|
||||
"""Initialize the cover."""
|
||||
self._name = name
|
||||
self._state = False
|
||||
@@ -79,7 +84,7 @@ class RPiGPIOCover(CoverDevice):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this cover."""
|
||||
return "{}.{}".format(self.__class__, self._name)
|
||||
return '{}.{}'.format(self.__class__, self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -6,37 +6,43 @@ https://home-assistant.io/components/cover.scsgate/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.scsgate as scsgate
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.cover import (CoverDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_DEVICES, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['scsgate']
|
||||
SCS_ID = 'scs_id'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICES): vol.Schema({cv.slug: scsgate.SCSGATE_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the SCSGate cover."""
|
||||
devices = config.get('devices')
|
||||
devices = config.get(CONF_DEVICES)
|
||||
covers = []
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if devices:
|
||||
for _, entity_info in devices.items():
|
||||
if entity_info[SCS_ID] in scsgate.SCSGATE.devices:
|
||||
if entity_info[scsgate.CONF_SCS_ID] in scsgate.SCSGATE.devices:
|
||||
continue
|
||||
|
||||
logger.info("Adding %s scsgate.cover", entity_info[CONF_NAME])
|
||||
|
||||
name = entity_info[CONF_NAME]
|
||||
scs_id = entity_info[SCS_ID]
|
||||
cover = SCSGateCover(
|
||||
name=name,
|
||||
scs_id=scs_id,
|
||||
logger=logger)
|
||||
scs_id = entity_info[scsgate.CONF_SCS_ID]
|
||||
|
||||
logger.info("Adding %s scsgate.cover", name)
|
||||
|
||||
cover = SCSGateCover(name=name, scs_id=scs_id, logger=logger)
|
||||
scsgate.SCSGATE.add_device(cover)
|
||||
covers.append(cover)
|
||||
|
||||
add_devices_callback(covers)
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
@@ -91,6 +97,5 @@ class SCSGateCover(CoverDevice):
|
||||
|
||||
def process_event(self, message):
|
||||
"""Handle a SCSGate message related with this cover."""
|
||||
self._logger.debug(
|
||||
"Rollershutter %s, got message %s",
|
||||
self._scs_id, message.toggled)
|
||||
self._logger.debug("Cover %s, got message %s",
|
||||
self._scs_id, message.toggled)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Support for Vera cover - curtains, rollershutters etc.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Find and return Vera covers."""
|
||||
add_devices_callback(
|
||||
VeraCover(device, VERA_CONTROLLER) for
|
||||
device in VERA_DEVICES['cover'])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class VeraCover(VeraDevice, CoverDevice):
|
||||
"""Represents a Vera Cover in Home Assistant."""
|
||||
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""
|
||||
Return current position of cover.
|
||||
|
||||
0 is closed, 100 is fully open.
|
||||
"""
|
||||
position = self.vera_device.get_level()
|
||||
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.vera_device.set_level(position)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self.vera_device.open()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self.vera_device.close()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self.vera_device.stop()
|
||||
@@ -4,46 +4,30 @@ Support for Wink Covers.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.13', 'pubnub==3.8.2']
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink cover platform."""
|
||||
import pywink
|
||||
|
||||
if discovery_info is None:
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if token is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing wink access_token. "
|
||||
"Get one at https://winkbearertoken.appspot.com/")
|
||||
return
|
||||
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
add_devices(WinkCoverDevice(shade) for shade, door in
|
||||
add_devices(WinkCoverDevice(shade) for shade in
|
||||
pywink.get_shades())
|
||||
add_devices(WinkCoverDevice(door) for door in
|
||||
pywink.get_garage_doors())
|
||||
|
||||
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""Representation of a Wink covers."""
|
||||
"""Representation of a Wink cover device."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the cover."""
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Wink Shades don't track their position."""
|
||||
return False
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the shade."""
|
||||
self.wink.set_state(0)
|
||||
|
||||
@@ -12,9 +12,6 @@ from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
|
||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
||||
|
||||
SOMFY = 0x47
|
||||
SOMFY_ZRTSI = 0x5a52
|
||||
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
|
||||
@@ -32,17 +29,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
|
||||
if (value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL and
|
||||
value.index == 0):
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \
|
||||
and value.index == 0:
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
elif (value.command_class == zwave.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class == zwave.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if value.type != zwave.TYPE_BOOL and \
|
||||
value.genre != zwave.GENRE_USER:
|
||||
elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \
|
||||
node.has_command_class(zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if value.type != zwave.const.TYPE_BOOL and \
|
||||
value.genre != zwave.const.GENRE_USER:
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
@@ -59,6 +56,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._lozwmgr = libopenzwave.PyManager()
|
||||
self._lozwmgr.create()
|
||||
self._node = value.node
|
||||
@@ -88,9 +86,10 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# Position value
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Level':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \
|
||||
value.label == 'Level':
|
||||
self._current_position = value.data
|
||||
|
||||
@property
|
||||
@@ -118,37 +117,40 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Open' or \
|
||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Down':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Open' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Down':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Up' or \
|
||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Close':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Up' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Close':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
self._node.set_dimmer(self._value.value_id, 100 - position)
|
||||
self._node.set_dimmer(self._value.value_id, position)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Open' or \
|
||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Down':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Open' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Down':
|
||||
self._lozwmgr.releaseButton(value.value_id)
|
||||
break
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ CONF_TRACK_NEW = 'track_new_devices'
|
||||
DEFAULT_TRACK_NEW = True
|
||||
|
||||
CONF_CONSIDER_HOME = 'consider_home'
|
||||
DEFAULT_CONSIDER_HOME = 180 # seconds
|
||||
DEFAULT_CONSIDER_HOME = 180
|
||||
|
||||
CONF_SCAN_INTERVAL = 'interval_seconds'
|
||||
DEFAULT_SCAN_INTERVAL = 12
|
||||
@@ -70,8 +70,10 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
|
||||
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
|
||||
vol.Schema({
|
||||
vol.Optional(CONF_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_CONSIDER_HOME): cv.positive_int # seconds
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_CONSIDER_HOME, default=timedelta(seconds=180)): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
}, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
@@ -118,8 +120,8 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
consider_home = timedelta(
|
||||
seconds=conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME))
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME,
|
||||
timedelta(seconds=DEFAULT_CONSIDER_HOME))
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
|
||||
devices = load_config(yaml_path, hass, consider_home)
|
||||
@@ -282,7 +284,7 @@ class Device(Entity):
|
||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track: bool, dev_id: str, mac: str, name: str=None,
|
||||
picture: str=None, gravatar: str=None,
|
||||
away_hide: bool=False) -> None:
|
||||
hide_if_away: bool=False) -> None:
|
||||
"""Initialize a device."""
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
@@ -307,7 +309,7 @@ class Device(Entity):
|
||||
else:
|
||||
self.config_picture = picture
|
||||
|
||||
self.away_hide = away_hide
|
||||
self.away_hide = hide_if_away
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -338,7 +340,7 @@ class Device(Entity):
|
||||
attr[ATTR_BATTERY] = self.battery
|
||||
|
||||
if self.attributes:
|
||||
for key, value in self.attributes:
|
||||
for key, value in self.attributes.items():
|
||||
attr[key] = value
|
||||
|
||||
return attr
|
||||
@@ -398,15 +400,29 @@ class Device(Entity):
|
||||
|
||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file."""
|
||||
dev_schema = vol.Schema({
|
||||
vol.Required('name'): cv.string,
|
||||
vol.Optional('track', default=False): cv.boolean,
|
||||
vol.Optional('mac', default=None): vol.Any(None, vol.All(cv.string,
|
||||
vol.Upper)),
|
||||
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
|
||||
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
|
||||
vol.Optional('picture', default=None): vol.Any(None, cv.string),
|
||||
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
try:
|
||||
return [
|
||||
Device(hass, consider_home, device.get('track', False),
|
||||
str(dev_id).lower(), None if device.get('mac') is None
|
||||
else str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get('gravatar'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
for dev_id, device in load_yaml_config_file(path).items()]
|
||||
result = []
|
||||
devices = load_yaml_config_file(path)
|
||||
for dev_id, device in devices.items():
|
||||
try:
|
||||
device = dev_schema(device)
|
||||
device['dev_id'] = cv.slugify(dev_id)
|
||||
except vol.Invalid as exp:
|
||||
log_exception(exp, dev_id, devices)
|
||||
else:
|
||||
result.append(Device(hass, **device))
|
||||
return result
|
||||
except (HomeAssistantError, FileNotFoundError):
|
||||
# When YAML file could not be loaded/did not contain a dict
|
||||
return []
|
||||
|
||||
@@ -15,12 +15,11 @@ from homeassistant.components.device_tracker import (PLATFORM_SCHEMA,
|
||||
ATTR_ATTRIBUTES)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle, datetime as dt_util
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import datetime as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_SECRET = 'secret'
|
||||
CONF_DEVICES = 'devices'
|
||||
@@ -53,7 +52,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def setup_scanner(hass, config: dict, see):
|
||||
"""Validate the configuration and return an Automatic scanner."""
|
||||
try:
|
||||
AutomaticDeviceScanner(config, see)
|
||||
AutomaticDeviceScanner(hass, config, see)
|
||||
except requests.HTTPError as err:
|
||||
_LOGGER.error(str(err))
|
||||
return False
|
||||
@@ -61,11 +60,14 @@ def setup_scanner(hass, config: dict, see):
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-few-public-methods
|
||||
class AutomaticDeviceScanner(object):
|
||||
"""A class representing an Automatic device."""
|
||||
|
||||
def __init__(self, config: dict, see) -> None:
|
||||
def __init__(self, hass, config: dict, see) -> None:
|
||||
"""Initialize the automatic device scanner."""
|
||||
self.hass = hass
|
||||
self._devices = config.get(CONF_DEVICES, None)
|
||||
self._access_token_payload = {
|
||||
'username': config.get(CONF_USERNAME),
|
||||
@@ -81,20 +83,10 @@ class AutomaticDeviceScanner(object):
|
||||
self.last_trips = {}
|
||||
self.see = see
|
||||
|
||||
self.scan_devices()
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return [item['id'] for item in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Get the device name from id."""
|
||||
vehicle = [item['display_name'] for item in self.last_results
|
||||
if item['id'] == device]
|
||||
|
||||
return vehicle[0]
|
||||
track_utc_time_change(self.hass, self._update_info,
|
||||
second=range(0, 60, 30))
|
||||
|
||||
def _update_headers(self):
|
||||
"""Get the access token from automatic."""
|
||||
@@ -114,10 +106,9 @@ class AutomaticDeviceScanner(object):
|
||||
'Authorization': 'Bearer {}'.format(access_token)
|
||||
}
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self) -> None:
|
||||
def _update_info(self, now=None) -> None:
|
||||
"""Update the device info."""
|
||||
_LOGGER.info('Updating devices')
|
||||
_LOGGER.debug('Updating devices %s', now)
|
||||
self._update_headers()
|
||||
|
||||
response = requests.get(URL_VEHICLES, headers=self._headers)
|
||||
@@ -142,6 +133,7 @@ class AutomaticDeviceScanner(object):
|
||||
|
||||
for vehicle in self.last_results:
|
||||
dev_id = vehicle.get('id')
|
||||
host_name = vehicle.get('display_name')
|
||||
|
||||
attrs = {
|
||||
'fuel_level': vehicle.get('fuel_level_percent')
|
||||
@@ -149,6 +141,7 @@ class AutomaticDeviceScanner(object):
|
||||
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
'host_name': host_name,
|
||||
'mac': dev_id,
|
||||
ATTR_ATTRIBUTES: attrs
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tracking for bluetooth devices."""
|
||||
"""Tracking for bluetooth low energy devices."""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
@@ -85,7 +85,9 @@ class FritzBoxScanner(object):
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name of the given device or None if is not known."""
|
||||
ret = self.fritz_box.get_specific_host_entry(mac)['NewHostName']
|
||||
ret = self.fritz_box.get_specific_host_entry(mac).get(
|
||||
'NewHostName'
|
||||
)
|
||||
if ret == {}:
|
||||
return None
|
||||
return ret
|
||||
|
||||
@@ -6,9 +6,11 @@ https://home-assistant.io/components/device_tracker.locative/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.device_tracker import ( # NOQA
|
||||
DOMAIN, PLATFORM_SCHEMA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,8 +27,8 @@ def setup_scanner(hass, config, see):
|
||||
class LocativeView(HomeAssistantView):
|
||||
"""View to handle locative requests."""
|
||||
|
||||
url = "/api/locative"
|
||||
name = "api:locative"
|
||||
url = '/api/locative'
|
||||
name = 'api:locative'
|
||||
|
||||
def __init__(self, hass, see):
|
||||
"""Initialize Locative url endpoints."""
|
||||
@@ -43,22 +45,22 @@ class LocativeView(HomeAssistantView):
|
||||
data = request.values
|
||||
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
return ("Latitude and longitude not specified.",
|
||||
return ('Latitude and longitude not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'device' not in data:
|
||||
_LOGGER.error("Device id not specified.")
|
||||
return ("Device id not specified.",
|
||||
_LOGGER.error('Device id not specified.')
|
||||
return ('Device id not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'id' not in data:
|
||||
_LOGGER.error("Location id not specified.")
|
||||
return ("Location id not specified.",
|
||||
_LOGGER.error('Location id not specified.')
|
||||
return ('Location id not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'trigger' not in data:
|
||||
_LOGGER.error("Trigger is not specified.")
|
||||
return ("Trigger is not specified.",
|
||||
_LOGGER.error('Trigger is not specified.')
|
||||
return ('Trigger is not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
@@ -67,15 +69,15 @@ class LocativeView(HomeAssistantView):
|
||||
|
||||
if direction == 'enter':
|
||||
self.see(dev_id=device, location_name=location_name)
|
||||
return "Setting location to {}".format(location_name)
|
||||
return 'Setting location to {}'.format(location_name)
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = self.hass.states.get(
|
||||
"{}.{}".format(DOMAIN, device))
|
||||
'{}.{}'.format(DOMAIN, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
self.see(dev_id=device, location_name=STATE_NOT_HOME)
|
||||
return "Setting location to not home"
|
||||
return 'Setting location to not home'
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered
|
||||
@@ -87,10 +89,10 @@ class LocativeView(HomeAssistantView):
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
return "Received test message."
|
||||
return 'Received test message.'
|
||||
|
||||
else:
|
||||
_LOGGER.error("Received unidentified message from Locative: %s",
|
||||
_LOGGER.error('Received unidentified message from Locative: %s',
|
||||
direction)
|
||||
return ("Received unidentified message: {}".format(direction),
|
||||
return ('Received unidentified message: {}'.format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
@@ -10,11 +10,13 @@ import subprocess
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_HOSTS
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle, convert
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
@@ -27,18 +29,21 @@ CONF_EXCLUDE = 'exclude'
|
||||
|
||||
REQUIREMENTS = ['python-nmap==0.6.1']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOSTS): cv.string,
|
||||
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_EXCLUDE, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Length(min=1))
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Nmap scanner."""
|
||||
if not validate_config(config, {DOMAIN: [CONF_HOSTS]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = NmapDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
Device = namedtuple("Device", ["mac", "name", "ip", "last_update"])
|
||||
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
|
||||
|
||||
|
||||
def _arp(ip_address):
|
||||
@@ -49,24 +54,26 @@ def _arp(ip_address):
|
||||
match = re.search(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})', str(out))
|
||||
if match:
|
||||
return match.group(0)
|
||||
_LOGGER.info("No MAC address found for %s", ip_address)
|
||||
_LOGGER.info('No MAC address found for %s', ip_address)
|
||||
return None
|
||||
|
||||
|
||||
class NmapDeviceScanner(object):
|
||||
"""This class scans for devices using nmap."""
|
||||
|
||||
exclude = []
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.last_results = []
|
||||
|
||||
self.hosts = config[CONF_HOSTS]
|
||||
self.exclude = config.get(CONF_EXCLUDE, [])
|
||||
minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0)
|
||||
minutes = config[CONF_HOME_INTERVAL]
|
||||
self.home_interval = timedelta(minutes=minutes)
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info("nmap scanner initialized")
|
||||
_LOGGER.info('nmap scanner initialized')
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
@@ -90,21 +97,18 @@ class NmapDeviceScanner(object):
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info("Scanning")
|
||||
_LOGGER.info('Scanning')
|
||||
|
||||
from nmap import PortScanner, PortScannerError
|
||||
scanner = PortScanner()
|
||||
|
||||
options = "-F --host-timeout 5s "
|
||||
exclude = "--exclude "
|
||||
options = '-F --host-timeout 5s '
|
||||
|
||||
if self.home_interval:
|
||||
boundary = dt_util.now() - self.home_interval
|
||||
last_results = [device for device in self.last_results
|
||||
if device.last_update > boundary]
|
||||
if last_results:
|
||||
# Pylint is confused here.
|
||||
# pylint: disable=no-member
|
||||
exclude_hosts = self.exclude + [device.ip for device
|
||||
in last_results]
|
||||
else:
|
||||
@@ -113,8 +117,7 @@ class NmapDeviceScanner(object):
|
||||
last_results = []
|
||||
exclude_hosts = self.exclude
|
||||
if exclude_hosts:
|
||||
exclude = " --exclude {}".format(",".join(exclude_hosts))
|
||||
options += exclude
|
||||
options += ' --exclude {}'.format(','.join(exclude_hosts))
|
||||
|
||||
try:
|
||||
result = scanner.scan(hosts=self.hosts, arguments=options)
|
||||
@@ -134,5 +137,5 @@ class NmapDeviceScanner(object):
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info("nmap scan successful")
|
||||
_LOGGER.info('nmap scan successful')
|
||||
return True
|
||||
|
||||
@@ -7,6 +7,7 @@ https://home-assistant.io/components/device_tracker.owntracks/
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -18,53 +19,121 @@ from homeassistant.util import convert, slugify
|
||||
from homeassistant.components import zone as zone_comp
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
REGIONS_ENTERED = defaultdict(list)
|
||||
MOBILE_BEACONS_ACTIVE = defaultdict(list)
|
||||
|
||||
BEACON_DEV_ID = 'beacon'
|
||||
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'
|
||||
REQUIREMENTS = ['libnacl==1.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LOCK = threading.Lock()
|
||||
BEACON_DEV_ID = 'beacon'
|
||||
|
||||
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
|
||||
CONF_SECRET = 'secret'
|
||||
CONF_WAYPOINT_IMPORT = 'waypoints'
|
||||
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
LOCK = threading.Lock()
|
||||
|
||||
MOBILE_BEACONS_ACTIVE = defaultdict(list)
|
||||
|
||||
REGIONS_ENTERED = defaultdict(list)
|
||||
|
||||
VALIDATE_LOCATION = 'location'
|
||||
VALIDATE_TRANSITION = 'transition'
|
||||
VALIDATE_WAYPOINTS = 'waypoints'
|
||||
|
||||
WAYPOINT_LAT_KEY = 'lat'
|
||||
WAYPOINT_LON_KEY = 'lon'
|
||||
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
|
||||
vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
|
||||
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(cv.ensure_list, [cv.string])
|
||||
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
|
||||
cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_SECRET): vol.Any(
|
||||
vol.Schema({vol.Optional(cv.string): cv.string}),
|
||||
cv.string)
|
||||
})
|
||||
|
||||
|
||||
def get_cipher():
|
||||
"""Return decryption function and length of key."""
|
||||
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
|
||||
from libnacl.secret import SecretBox
|
||||
|
||||
def decrypt(ciphertext, key):
|
||||
"""Decrypt ciphertext using key."""
|
||||
return SecretBox(key).decrypt(ciphertext)
|
||||
return (KEYLEN, decrypt)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup an OwnTracks tracker."""
|
||||
"""Set up an OwnTracks tracker."""
|
||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
||||
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
||||
secret = config.get(CONF_SECRET)
|
||||
|
||||
def decrypt_payload(topic, ciphertext):
|
||||
"""Decrypt encrypted payload."""
|
||||
try:
|
||||
keylen, decrypt = get_cipher()
|
||||
except OSError:
|
||||
_LOGGER.warning('Ignoring encrypted payload '
|
||||
'because libsodium not installed.')
|
||||
return None
|
||||
|
||||
if isinstance(secret, dict):
|
||||
key = secret.get(topic)
|
||||
else:
|
||||
key = secret
|
||||
|
||||
if key is None:
|
||||
_LOGGER.warning('Ignoring encrypted payload '
|
||||
'because no decryption key known '
|
||||
'for topic %s.', topic)
|
||||
return None
|
||||
|
||||
key = key.encode("utf-8")
|
||||
key = key[:keylen]
|
||||
key = key.ljust(keylen, b'\0')
|
||||
|
||||
try:
|
||||
ciphertext = base64.b64decode(ciphertext)
|
||||
message = decrypt(ciphertext, key)
|
||||
message = message.decode("utf-8")
|
||||
_LOGGER.debug("Decrypted payload: %s", message)
|
||||
return message
|
||||
except ValueError:
|
||||
_LOGGER.warning('Ignoring encrypted payload '
|
||||
'because unable to decrypt using key '
|
||||
'for topic %s.', topic)
|
||||
return None
|
||||
|
||||
def validate_payload(topic, payload, data_type):
|
||||
"""Validate the OwnTracks payload."""
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
def validate_payload(payload, data_type):
|
||||
"""Validate OwnTracks payload."""
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
_LOGGER.error('Unable to parse payload as JSON: %s', payload)
|
||||
return None
|
||||
|
||||
if isinstance(data, dict) and \
|
||||
data.get('_type') == 'encrypted' and \
|
||||
'data' in data:
|
||||
plaintext_payload = decrypt_payload(topic, data['data'])
|
||||
if plaintext_payload is None:
|
||||
return None
|
||||
else:
|
||||
return validate_payload(topic, plaintext_payload, data_type)
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != data_type:
|
||||
_LOGGER.debug('Skipping %s update for following data '
|
||||
'because of missing or malformatted data: %s',
|
||||
@@ -90,7 +159,7 @@ def setup_scanner(hass, config, see):
|
||||
"""MQTT message received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||
data = validate_payload(payload, VALIDATE_LOCATION)
|
||||
data = validate_payload(topic, payload, VALIDATE_LOCATION)
|
||||
if not data:
|
||||
return
|
||||
|
||||
@@ -111,7 +180,7 @@ def setup_scanner(hass, config, see):
|
||||
"""MQTT event (geofences) received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||
data = validate_payload(payload, VALIDATE_TRANSITION)
|
||||
data = validate_payload(topic, payload, VALIDATE_TRANSITION)
|
||||
if not data:
|
||||
return
|
||||
|
||||
@@ -206,7 +275,7 @@ def setup_scanner(hass, config, see):
|
||||
"""List of waypoints published by a user."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typewaypoints
|
||||
data = validate_payload(payload, VALIDATE_WAYPOINTS)
|
||||
data = validate_payload(topic, payload, VALIDATE_WAYPOINTS)
|
||||
if not data:
|
||||
return
|
||||
|
||||
@@ -218,9 +287,18 @@ def setup_scanner(hass, config, see):
|
||||
lat = wayp[WAYPOINT_LAT_KEY]
|
||||
lon = wayp[WAYPOINT_LON_KEY]
|
||||
rad = wayp['rad']
|
||||
|
||||
# check zone exists
|
||||
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
|
||||
|
||||
# Check if state already exists
|
||||
if hass.states.get(entity_id) is not None:
|
||||
continue
|
||||
|
||||
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
||||
zone_comp.ICON_IMPORT, False, True)
|
||||
zone_comp.add_zone(hass, pretty_name, zone)
|
||||
zone_comp.ICON_IMPORT, False)
|
||||
zone.entity_id = entity_id
|
||||
zone.update_ha_state()
|
||||
|
||||
def see_beacons(dev_id, kwargs_param):
|
||||
"""Set active beacons to the current location."""
|
||||
|
||||
@@ -10,9 +10,11 @@ import telnetlib
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
@@ -21,23 +23,24 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DEVICES_REGEX = re.compile(
|
||||
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' +
|
||||
r'(?P<status>([^\s]+))\s+' +
|
||||
r'(?P<type>([^\s]+))\s+' +
|
||||
r'(?P<intf>([^\s]+))\s+' +
|
||||
r'(?P<hwintf>([^\s]+))\s+' +
|
||||
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s'
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+'
|
||||
r'(?P<status>([^\s]+))\s+'
|
||||
r'(?P<type>([^\s]+))\s+'
|
||||
r'(?P<intf>([^\s]+))\s+'
|
||||
r'(?P<hwintf>([^\s]+))\s+'
|
||||
r'(?P<host>([^\s]+))')
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a THOMSON scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = ThomsonDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
@@ -84,7 +87,7 @@ class ThomsonDeviceScanner(object):
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Checking ARP")
|
||||
_LOGGER.info('Checking ARP')
|
||||
data = self.get_thomson_data()
|
||||
if not data:
|
||||
return False
|
||||
@@ -108,11 +111,11 @@ class ThomsonDeviceScanner(object):
|
||||
devices_result = telnet.read_until(b'=>').split(b'\r\n')
|
||||
telnet.write('exit\r\n'.encode('ascii'))
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
_LOGGER.exception('Unexpected response from router')
|
||||
return
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router," +
|
||||
" is telnet enabled?")
|
||||
_LOGGER.exception('Connection refused by router,'
|
||||
' is telnet enabled?')
|
||||
return
|
||||
|
||||
devices = {}
|
||||
|
||||
@@ -12,10 +12,11 @@ import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
@@ -23,26 +24,22 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a TP-Link scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
return None
|
||||
for cls in [Tplink4DeviceScanner, Tplink3DeviceScanner,
|
||||
Tplink2DeviceScanner, TplinkDeviceScanner]:
|
||||
scanner = cls(config[DOMAIN])
|
||||
if scanner.success_init:
|
||||
return scanner
|
||||
|
||||
scanner = Tplink4DeviceScanner(config[DOMAIN])
|
||||
|
||||
if not scanner.success_init:
|
||||
scanner = Tplink3DeviceScanner(config[DOMAIN])
|
||||
|
||||
if not scanner.success_init:
|
||||
scanner = Tplink2DeviceScanner(config[DOMAIN])
|
||||
|
||||
if not scanner.success_init:
|
||||
scanner = TplinkDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
return None
|
||||
|
||||
|
||||
class TplinkDeviceScanner(object):
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Support for Volvo On Call.
|
||||
|
||||
http://www.volvocars.com/intl/own/owner-info/volvo-on-call
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.volvooncall/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urljoin
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME)
|
||||
from homeassistant.components.device_tracker import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
PLATFORM_SCHEMA)
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_URL = 'https://vocapi.wirelesscar.net/customerapi/rest/v3.0/'
|
||||
HEADERS = {"X-Device-Id": "Device",
|
||||
"X-OS-Type": "Android",
|
||||
"X-Originator-Type": "App"}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Validate the configuration and return a scanner."""
|
||||
session = requests.Session()
|
||||
session.headers.update(HEADERS)
|
||||
session.auth = (config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD))
|
||||
|
||||
interval = max(MIN_TIME_BETWEEN_SCANS.seconds,
|
||||
config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
|
||||
|
||||
def query(ref, rel=SERVICE_URL):
|
||||
"""Perform a query to the online service."""
|
||||
url = urljoin(rel, ref)
|
||||
_LOGGER.debug("Request for %s", url)
|
||||
res = session.get(url, timeout=15)
|
||||
res.raise_for_status()
|
||||
_LOGGER.debug("Received %s", res.json())
|
||||
return res.json()
|
||||
|
||||
def update(now):
|
||||
"""Update status from the online service."""
|
||||
try:
|
||||
_LOGGER.debug("Updating")
|
||||
status = query("status", vehicle_url)
|
||||
position = query("position", vehicle_url)
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=(position["position"]["latitude"],
|
||||
position["position"]["longitude"]),
|
||||
attributes=dict(
|
||||
tank_volume=attributes["fuelTankVolume"],
|
||||
washer_fluid=status["washerFluidLevel"],
|
||||
brake_fluid=status["brakeFluid"],
|
||||
service_warning=status["serviceWarningStatus"],
|
||||
fuel=status["fuelAmount"],
|
||||
odometer=status["odometer"],
|
||||
range=status["distanceToEmpty"]))
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error("Could not query server: %s", error)
|
||||
finally:
|
||||
track_point_in_utc_time(hass, update,
|
||||
now + timedelta(seconds=interval))
|
||||
|
||||
try:
|
||||
_LOGGER.info('Logging in to service')
|
||||
user = query("customeraccounts")
|
||||
rel = query(user["accountVehicleRelations"][0])
|
||||
vehicle_url = rel["vehicle"] + '/'
|
||||
attributes = query("attributes", vehicle_url)
|
||||
|
||||
dev_id = "volvo_" + slugify(attributes["registrationNumber"])
|
||||
host_name = "%s %s/%s" % (attributes["registrationNumber"],
|
||||
attributes["vehicleType"],
|
||||
attributes["modelYear"])
|
||||
update(utcnow())
|
||||
return True
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error("Could not log in to service. "
|
||||
"Please check configuration: "
|
||||
"%s", error)
|
||||
return False
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Support for Digital Ocean.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/digital_ocean/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-digitalocean==1.9.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CREATED_AT = 'created_at'
|
||||
ATTR_DROPLET_ID = 'droplet_id'
|
||||
ATTR_DROPLET_NAME = 'droplet_name'
|
||||
ATTR_FEATURES = 'features'
|
||||
ATTR_IPV4_ADDRESS = 'ipv4_address'
|
||||
ATTR_IPV6_ADDRESS = 'ipv6_address'
|
||||
ATTR_MEMORY = 'memory'
|
||||
ATTR_REGION = 'region'
|
||||
ATTR_VCPUS = 'vcpus'
|
||||
|
||||
CONF_DROPLETS = 'droplets'
|
||||
|
||||
DIGITAL_OCEAN = None
|
||||
DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor']
|
||||
DOMAIN = 'digital_ocean'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument,too-few-public-methods
|
||||
def setup(hass, config):
|
||||
"""Setup the Digital Ocean component."""
|
||||
conf = config[DOMAIN]
|
||||
access_token = conf.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
global DIGITAL_OCEAN
|
||||
DIGITAL_OCEAN = DigitalOcean(access_token)
|
||||
|
||||
if not DIGITAL_OCEAN.manager.get_account():
|
||||
_LOGGER.error("No Digital Ocean account found for the given API Token")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DigitalOcean(object):
|
||||
"""Handle all communication with the Digital Ocean API."""
|
||||
|
||||
def __init__(self, access_token):
|
||||
"""Initialize the Digital Ocean connection."""
|
||||
import digitalocean
|
||||
|
||||
self._access_token = access_token
|
||||
self.data = None
|
||||
self.manager = digitalocean.Manager(token=self._access_token)
|
||||
|
||||
def get_droplet_id(self, droplet_name):
|
||||
"""Get the status of a Digital Ocean droplet."""
|
||||
droplet_id = None
|
||||
|
||||
all_droplets = self.manager.get_all_droplets()
|
||||
for droplet in all_droplets:
|
||||
if droplet_name == droplet.name:
|
||||
droplet_id = droplet.id
|
||||
|
||||
return droplet_id
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Use the data from Digital Ocean API."""
|
||||
self.data = self.manager.get_all_droplets()
|
||||
@@ -9,6 +9,8 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
@@ -33,6 +35,10 @@ SERVICE_HANDLERS = {
|
||||
'directv': ('media_player', 'directv'),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Start a discovery service."""
|
||||
|
||||
Executable → Regular
+545
-542
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['pyenvisalink==1.0', 'pydispatcher==2.0.5']
|
||||
REQUIREMENTS = ['pyenvisalink==1.7', 'pydispatcher==2.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'envisalink'
|
||||
@@ -34,12 +34,14 @@ CONF_PARTITIONS = 'partitions'
|
||||
CONF_ZONENAME = 'name'
|
||||
CONF_ZONETYPE = 'type'
|
||||
CONF_PARTITIONNAME = 'name'
|
||||
CONF_PANIC = 'panic_type'
|
||||
|
||||
DEFAULT_PORT = 4025
|
||||
DEFAULT_EVL_VERSION = 3
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_ZONEDUMP_INTERVAL = 30
|
||||
DEFAULT_ZONETYPE = 'opening'
|
||||
DEFAULT_PANIC = 'Police'
|
||||
|
||||
SIGNAL_ZONE_UPDATE = 'zones_updated'
|
||||
SIGNAL_PARTITION_UPDATE = 'partition_updated'
|
||||
@@ -60,6 +62,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASS): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_PANIC, default=DEFAULT_PANIC): cv.string,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
|
||||
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT): cv.port,
|
||||
@@ -89,6 +92,7 @@ def setup(hass, base_config):
|
||||
_port = config.get(CONF_EVL_PORT)
|
||||
_code = config.get(CONF_CODE)
|
||||
_panel_type = config.get(CONF_PANEL_TYPE)
|
||||
_panic_type = config.get(CONF_PANIC)
|
||||
_version = config.get(CONF_EVL_VERSION)
|
||||
_user = config.get(CONF_USERNAME)
|
||||
_pass = config.get(CONF_PASS)
|
||||
@@ -104,7 +108,8 @@ def setup(hass, base_config):
|
||||
_user,
|
||||
_pass,
|
||||
_zone_dump,
|
||||
_keep_alive)
|
||||
_keep_alive,
|
||||
hass.loop)
|
||||
|
||||
def login_fail_callback(data):
|
||||
"""Callback for when the evl rejects our login."""
|
||||
@@ -149,7 +154,7 @@ def setup(hass, base_config):
|
||||
|
||||
def start_envisalink(event):
|
||||
"""Startup process for the Envisalink."""
|
||||
EVL_CONTROLLER.start()
|
||||
hass.loop.call_soon_threadsafe(EVL_CONTROLLER.start)
|
||||
for _ in range(10):
|
||||
if 'success' in _connect_status:
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
|
||||
@@ -177,14 +182,15 @@ def setup(hass, base_config):
|
||||
# Load sub-components for Envisalink
|
||||
if _partitions:
|
||||
load_platform(hass, 'alarm_control_panel', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
{CONF_PARTITIONS: _partitions,
|
||||
CONF_CODE: _code,
|
||||
CONF_PANIC: _panic_type}, base_config)
|
||||
load_platform(hass, 'sensor', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
{CONF_PARTITIONS: _partitions,
|
||||
CONF_CODE: _code}, base_config)
|
||||
if _zones:
|
||||
load_platform(hass, 'binary_sensor', 'envisalink',
|
||||
{'zones': _zones}, config)
|
||||
{CONF_ZONES: _zones}, base_config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Support for ISY994 fans.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.isy994/
|
||||
"""
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF,
|
||||
SPEED_LOW, SPEED_MED,
|
||||
SPEED_HIGH)
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_UNKNOWN, STATE_ON, STATE_OFF
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
0: SPEED_OFF,
|
||||
63: SPEED_LOW,
|
||||
64: SPEED_LOW,
|
||||
190: SPEED_MED,
|
||||
191: SPEED_MED,
|
||||
255: SPEED_HIGH,
|
||||
}
|
||||
|
||||
STATE_TO_VALUE = {}
|
||||
for key in VALUE_TO_STATE:
|
||||
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
|
||||
|
||||
STATES = [SPEED_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Setup the ISY994 fan platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error('A connection has not been made to the ISY controller.')
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.NODES, states=STATES):
|
||||
devices.append(ISYFanDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
actions = program[isy.KEY_ACTIONS]
|
||||
assert actions.dtype == 'program', 'Not a program'
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
devices.append(ISYFanProgram(program.name, status, actions))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYFanDevice(isy.ISYDevice, FanEntity):
|
||||
"""Representation of an ISY994 fan device."""
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 fan device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
self.speed = self.state
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 fan device."""
|
||||
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
|
||||
|
||||
def set_speed(self, speed: str) -> None:
|
||||
"""Send the set speed command to the ISY994 fan device."""
|
||||
if not self._node.on(val=STATE_TO_VALUE.get(speed, 0)):
|
||||
_LOGGER.debug('Unable to set fan speed')
|
||||
else:
|
||||
self.speed = self.state
|
||||
|
||||
def turn_on(self, speed: str=None, **kwargs) -> None:
|
||||
"""Send the turn on command to the ISY994 fan device."""
|
||||
self.set_speed(speed)
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn off command to the ISY994 fan device."""
|
||||
if not self._node.off():
|
||||
_LOGGER.debug('Unable to set fan speed')
|
||||
else:
|
||||
self.speed = self.state
|
||||
|
||||
|
||||
class ISYFanProgram(ISYFanDevice):
|
||||
"""Representation of an ISY994 fan program."""
|
||||
|
||||
def __init__(self, name: str, node, actions) -> None:
|
||||
"""Initialize the ISY994 fan program."""
|
||||
ISYFanDevice.__init__(self, node)
|
||||
self._name = name
|
||||
self._actions = actions
|
||||
self.speed = STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 fan program."""
|
||||
return STATE_ON if bool(self.value) else STATE_OFF
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn on command to ISY994 fan program."""
|
||||
if not self._actions.runThen():
|
||||
_LOGGER.error('Unable to open the cover')
|
||||
else:
|
||||
self.speed = STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Send the turn off command to ISY994 fan program."""
|
||||
if not self._actions.runElse():
|
||||
_LOGGER.error('Unable to close the cover')
|
||||
else:
|
||||
self.speed = STATE_ON if self.is_on else STATE_OFF
|
||||
@@ -5,17 +5,16 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/fan.mqtt/
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import (CONF_NAME, CONF_OPTIMISTIC, CONF_STATE,
|
||||
STATE_ON, STATE_OFF)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF,
|
||||
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.template import render_with_possible_json_value
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_MEDIUM,
|
||||
SPEED_HIGH, FanEntity,
|
||||
SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
|
||||
@@ -23,33 +22,31 @@ from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_MEDIUM,
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["mqtt"]
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_STATE_VALUE_TEMPLATE = "state_value_template"
|
||||
CONF_SPEED_STATE_TOPIC = "speed_state_topic"
|
||||
CONF_SPEED_COMMAND_TOPIC = "speed_command_topic"
|
||||
CONF_SPEED_VALUE_TEMPLATE = "speed_value_template"
|
||||
CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"
|
||||
CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"
|
||||
CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template"
|
||||
CONF_PAYLOAD_ON = "payload_on"
|
||||
CONF_PAYLOAD_OFF = "payload_off"
|
||||
CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on"
|
||||
CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off"
|
||||
CONF_PAYLOAD_LOW_SPEED = "payload_low_speed"
|
||||
CONF_PAYLOAD_MEDIUM_SPEED = "payload_medium_speed"
|
||||
CONF_PAYLOAD_HIGH_SPEED = "payload_high_speed"
|
||||
CONF_SPEED_LIST = "speeds"
|
||||
CONF_STATE_VALUE_TEMPLATE = 'state_value_template'
|
||||
CONF_SPEED_STATE_TOPIC = 'speed_state_topic'
|
||||
CONF_SPEED_COMMAND_TOPIC = 'speed_command_topic'
|
||||
CONF_SPEED_VALUE_TEMPLATE = 'speed_value_template'
|
||||
CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic'
|
||||
CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic'
|
||||
CONF_OSCILLATION_VALUE_TEMPLATE = 'oscillation_value_template'
|
||||
CONF_PAYLOAD_OSCILLATION_ON = 'payload_oscillation_on'
|
||||
CONF_PAYLOAD_OSCILLATION_OFF = 'payload_oscillation_off'
|
||||
CONF_PAYLOAD_LOW_SPEED = 'payload_low_speed'
|
||||
CONF_PAYLOAD_MEDIUM_SPEED = 'payload_medium_speed'
|
||||
CONF_PAYLOAD_HIGH_SPEED = 'payload_high_speed'
|
||||
CONF_SPEED_LIST = 'speeds'
|
||||
|
||||
DEFAULT_NAME = "MQTT Fan"
|
||||
DEFAULT_PAYLOAD_ON = "ON"
|
||||
DEFAULT_PAYLOAD_OFF = "OFF"
|
||||
DEFAULT_NAME = 'MQTT Fan'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
OSCILLATE_ON_PAYLOAD = "oscillate_on"
|
||||
OSCILLATE_OFF_PAYLOAD = "oscillate_off"
|
||||
OSCILLATE_ON_PAYLOAD = 'oscillate_on'
|
||||
OSCILLATE_OFF_PAYLOAD = 'oscillate_off'
|
||||
|
||||
OSCILLATION = "oscillation"
|
||||
OSCILLATION = 'oscillation'
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
@@ -77,11 +74,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup MQTT fan platform."""
|
||||
add_devices_callback([MqttFan(
|
||||
add_devices([MqttFan(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_STATE_TOPIC,
|
||||
@@ -97,19 +94,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
ATTR_SPEED: config.get(CONF_SPEED_VALUE_TEMPLATE),
|
||||
OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE)
|
||||
},
|
||||
config[CONF_QOS],
|
||||
config[CONF_RETAIN],
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
{
|
||||
STATE_ON: config[CONF_PAYLOAD_ON],
|
||||
STATE_OFF: config[CONF_PAYLOAD_OFF],
|
||||
OSCILLATE_ON_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_ON],
|
||||
OSCILLATE_OFF_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_OFF],
|
||||
SPEED_LOW: config[CONF_PAYLOAD_LOW_SPEED],
|
||||
SPEED_MEDIUM: config[CONF_PAYLOAD_MEDIUM_SPEED],
|
||||
SPEED_HIGH: config[CONF_PAYLOAD_HIGH_SPEED],
|
||||
STATE_ON: config.get(CONF_PAYLOAD_ON),
|
||||
STATE_OFF: config.get(CONF_PAYLOAD_OFF),
|
||||
OSCILLATE_ON_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_ON),
|
||||
OSCILLATE_OFF_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_OFF),
|
||||
SPEED_LOW: config.get(CONF_PAYLOAD_LOW_SPEED),
|
||||
SPEED_MEDIUM: config.get(CONF_PAYLOAD_MEDIUM_SPEED),
|
||||
SPEED_HIGH: config.get(CONF_PAYLOAD_HIGH_SPEED),
|
||||
},
|
||||
config[CONF_SPEED_LIST],
|
||||
config[CONF_OPTIMISTIC],
|
||||
config.get(CONF_SPEED_LIST),
|
||||
config.get(CONF_OPTIMISTIC),
|
||||
)])
|
||||
|
||||
|
||||
@@ -120,7 +117,7 @@ class MqttFan(FanEntity):
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, topic, templates, qos, retain, payload,
|
||||
speed_list, optimistic):
|
||||
"""Initialize MQTT fan."""
|
||||
"""Initialize the MQTT fan."""
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
@@ -129,11 +126,10 @@ class MqttFan(FanEntity):
|
||||
self._payload = payload
|
||||
self._speed_list = speed_list
|
||||
self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None
|
||||
self._optimistic_oscillation = (optimistic or
|
||||
topic[CONF_OSCILLATION_STATE_TOPIC]
|
||||
is None)
|
||||
self._optimistic_speed = (optimistic or
|
||||
topic[CONF_SPEED_STATE_TOPIC] is None)
|
||||
self._optimistic_oscillation = (
|
||||
optimistic or topic[CONF_OSCILLATION_STATE_TOPIC] is None)
|
||||
self._optimistic_speed = (
|
||||
optimistic or topic[CONF_SPEED_STATE_TOPIC] is None)
|
||||
self._state = False
|
||||
self._supported_features = 0
|
||||
self._supported_features |= (topic[CONF_OSCILLATION_STATE_TOPIC]
|
||||
@@ -141,9 +137,12 @@ class MqttFan(FanEntity):
|
||||
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC]
|
||||
is not None and SUPPORT_SET_SPEED)
|
||||
|
||||
templates = {key: ((lambda value: value) if tpl is None else
|
||||
partial(render_with_possible_json_value, hass, tpl))
|
||||
for key, tpl in templates.items()}
|
||||
for key, tpl in list(templates.items()):
|
||||
if tpl is None:
|
||||
templates[key] = lambda value: value
|
||||
else:
|
||||
tpl.hass = hass
|
||||
templates[key] = tpl.render_with_possible_json_value
|
||||
|
||||
def state_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Component that will help set the ffmpeg component.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ffmpeg/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'ffmpeg'
|
||||
REQUIREMENTS = ["ha-ffmpeg==0.13"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_INPUT = 'input'
|
||||
CONF_FFMPEG_BIN = 'ffmpeg_bin'
|
||||
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
|
||||
CONF_OUTPUT = 'output'
|
||||
CONF_RUN_TEST = 'run_test'
|
||||
|
||||
DEFAULT_BINARY = 'ffmpeg'
|
||||
DEFAULT_RUN_TEST = True
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string,
|
||||
vol.Optional(CONF_RUN_TEST, default=DEFAULT_RUN_TEST): cv.boolean,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
FFMPEG_CONFIG = {
|
||||
CONF_FFMPEG_BIN: DEFAULT_BINARY,
|
||||
CONF_RUN_TEST: DEFAULT_RUN_TEST,
|
||||
}
|
||||
FFMPEG_TEST_CACHE = {}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the FFmpeg component."""
|
||||
if DOMAIN in config:
|
||||
FFMPEG_CONFIG.update(config.get(DOMAIN))
|
||||
return True
|
||||
|
||||
|
||||
def get_binary():
|
||||
"""Return ffmpeg binary from config."""
|
||||
return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN)
|
||||
|
||||
|
||||
def run_test(input_source):
|
||||
"""Run test on this input. TRUE is deactivate or run correct."""
|
||||
from haffmpeg import Test
|
||||
|
||||
if FFMPEG_CONFIG.get(CONF_RUN_TEST):
|
||||
# if in cache
|
||||
if input_source in FFMPEG_TEST_CACHE:
|
||||
return FFMPEG_TEST_CACHE[input_source]
|
||||
|
||||
# run test
|
||||
test = Test(get_binary())
|
||||
if not test.run_test(input_source):
|
||||
_LOGGER.error("FFmpeg '%s' test fails!", input_source)
|
||||
FFMPEG_TEST_CACHE[input_source] = False
|
||||
return False
|
||||
FFMPEG_TEST_CACHE[input_source] = True
|
||||
return True
|
||||
@@ -1,14 +1,14 @@
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "1fd10c1fcdf56a61f60cf861d5a0368c",
|
||||
"frontend.html": "20defe06c11b2fa2f076dc92b6c3b0dd",
|
||||
"mdi.html": "710b84acc99b32514f52291aba9cd8e8",
|
||||
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
|
||||
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
||||
"panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa",
|
||||
"panels/ha-panel-dev-state.html": "4608326978256644c42b13940c028e0a",
|
||||
"panels/ha-panel-dev-template.html": "0a099d4589636ed3038a3e9f020468a7",
|
||||
"core.js": "9b3e5ab4eac7e3b074e0daf3f619a638",
|
||||
"frontend.html": "5854807d361de26fe93ad474010f19d2",
|
||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
|
||||
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
||||
"panels/ha-panel-dev-service.html": "c7974458ebc33412d95497e99b785e12",
|
||||
"panels/ha-panel-dev-state.html": "4be627b74e683af14ef779d8203ec674",
|
||||
"panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2",
|
||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:24px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
|
||||
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/balloob/home-assistant" target="_blank">server</a> — <a href="https://github.com/balloob/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/balloob/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
||||
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p>Path to configuration.yaml: [[hassConfigDir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a> — <a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/home-assistant/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},hassConfigDir:{type:String,bindNuclear:function(r){return r.configGetters.configDir}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user