Compare commits
358 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f046972d9 | |||
| 53f8828181 | |||
| b6b40286ef | |||
| 8a86ec5b74 | |||
| 20c5f9de4b | |||
| 61730012d8 | |||
| 25d2df5689 | |||
| 672b83db8a | |||
| b37438ebb7 | |||
| 902b72ba1a | |||
| f10fede17f | |||
| c9548b11b1 | |||
| f4aec3ac88 | |||
| e7425e9808 | |||
| 978b539111 | |||
| 9f4cd5fafe | |||
| ba3c9f9765 | |||
| 4ee8be52fe | |||
| 866bf887d3 | |||
| dddbce82f5 | |||
| be15ca3f23 | |||
| 9a305c9742 | |||
| de231cf9ab | |||
| 8325f9db8a | |||
| 3f38b9e52f | |||
| 1cb2a6add0 | |||
| acf75b5253 | |||
| 970bde9e99 | |||
| 796143a6c6 | |||
| 5569ae38f1 | |||
| 7eaad4fb3a | |||
| 35c679a956 | |||
| 678f273002 | |||
| 4e91c65d6e | |||
| f6106706e5 | |||
| ecf337b123 | |||
| f5d8327d9a | |||
| 30d4c54187 | |||
| edf20f542a | |||
| 9778000e9a | |||
| b5149dfba6 | |||
| ced3cd2616 | |||
| 7050236a61 | |||
| c8e1ffad89 | |||
| e3edff8a72 | |||
| d6fd0f405e | |||
| 1ab47b5d2b | |||
| a2365eccf6 | |||
| c46ba3446d | |||
| 5714f156c3 | |||
| 959dd29c90 | |||
| e75a66ed20 | |||
| 3e72aa8643 | |||
| 5aaa1f8404 | |||
| 7292e564f8 | |||
| 509cfb6433 | |||
| acdab67c1b | |||
| d7addf59cd | |||
| ccf9edf815 | |||
| 2fd3c186e2 | |||
| 719199da45 | |||
| a3cd7d653d | |||
| aeeb927e19 | |||
| a3a14f9ea4 | |||
| f4e7b231bc | |||
| 5f68735375 | |||
| 774fd19638 | |||
| e265401cd0 | |||
| 5b3dc7f2a5 | |||
| 9ef084d903 | |||
| 95b1e257bb | |||
| b06cf87c74 | |||
| 326337777a | |||
| 2c8a06bfbe | |||
| 198a234468 | |||
| e94aa3afe9 | |||
| 96e22c7b41 | |||
| 33450c726d | |||
| 97b9d3bd21 | |||
| fff589eeab | |||
| 5e5d2e8ab8 | |||
| 1a7ffdca52 | |||
| cada74df22 | |||
| bd3fbe8363 | |||
| 4d9c7d9684 | |||
| 0bf66384ed | |||
| c7798ef43c | |||
| f4d8095e54 | |||
| 5529d77c62 | |||
| c4e151f621 | |||
| f58941a0d4 | |||
| bca673f039 | |||
| 2687f2f623 | |||
| 134b3d2f3b | |||
| f450c1351c | |||
| c6b10f3703 | |||
| 4a08067b9c | |||
| 9c37437a59 | |||
| 9330142987 | |||
| 2bbaac44d4 | |||
| 253dee8e4d | |||
| 5722cf53bf | |||
| 5d301590c3 | |||
| 353f5d6b49 | |||
| 11da7bed12 | |||
| a358c8e10d | |||
| 58826b264a | |||
| 56abc7f9b4 | |||
| 55d60a6a13 | |||
| 5183cb5903 | |||
| 0aa8933df6 | |||
| fc46a24996 | |||
| 5be58bd056 | |||
| 4a423e63f3 | |||
| a8b32edc8e | |||
| 4a5b9db394 | |||
| 9c23178457 | |||
| 764d31efcb | |||
| fc90ccea36 | |||
| aab63ea22a | |||
| 157ab77232 | |||
| 9a86ccaaea | |||
| 32dd815852 | |||
| 9ac3928600 | |||
| 62e57456e1 | |||
| 11f11481b2 | |||
| 10f5e9744b | |||
| b2a2193ba3 | |||
| c8b2ba6559 | |||
| 1496f1a570 | |||
| 493c0bbb4c | |||
| 13dd17b2ab | |||
| 0ef1c3af34 | |||
| 44da43065f | |||
| ffb1613d55 | |||
| b0a2909835 | |||
| b5fb558c62 | |||
| 846a0513c7 | |||
| 0f1cad24ce | |||
| 24630b1ebe | |||
| b705b3ddb9 | |||
| 330d352d3a | |||
| bdaae6f844 | |||
| 49308bec13 | |||
| edd96c2b04 | |||
| 11c4d3892f | |||
| 75817ad46d | |||
| 780cdd5f90 | |||
| d70eaeb118 | |||
| 9e9c6d0184 | |||
| 2cdc0febf5 | |||
| c721ec7ab0 | |||
| 4e63e8328e | |||
| 8f2009a187 | |||
| 529eee994b | |||
| 3f7dd7ed9a | |||
| b952cfe705 | |||
| b9cf4df557 | |||
| 7892297240 | |||
| 20fcd1f0e2 | |||
| 8bbf13ef9f | |||
| 855756cb2a | |||
| bbcfb9158a | |||
| bcd4def0ae | |||
| ddc260b628 | |||
| 06bc062221 | |||
| 21f3b62d09 | |||
| 185ccc4fc4 | |||
| 2d337fd29a | |||
| c625e219c7 | |||
| b1736994b7 | |||
| 21feff5fd8 | |||
| 93118fcade | |||
| 5e1c74b430 | |||
| 3d3a0a7a4f | |||
| fdd1957750 | |||
| 2b97449d98 | |||
| c937a7bcb0 | |||
| e7f442d66b | |||
| 2c5d3387f2 | |||
| bb4f23f8e7 | |||
| 629b2e81ba | |||
| 3508f74fb2 | |||
| d16bc632da | |||
| 44d4987536 | |||
| 470702261a | |||
| 5fb7aa212b | |||
| 9522fe3a92 | |||
| ff3c90fb80 | |||
| 90ad54da7d | |||
| 2baa838ba7 | |||
| a8add06a40 | |||
| 1b23b32817 | |||
| eaaa0442e2 | |||
| bc9f2d21c4 | |||
| 1a139234af | |||
| d5435cf066 | |||
| 46ec6d6dce | |||
| 660e777f01 | |||
| de038bae65 | |||
| 7774f0ae53 | |||
| 7655b6271d | |||
| 10bf659773 | |||
| e8a22cb4a8 | |||
| 2650c73a89 | |||
| bdf948d866 | |||
| 928e025910 | |||
| 96aae1292b | |||
| c0bf3d7f32 | |||
| 307514e3a7 | |||
| b939626497 | |||
| 1522e67351 | |||
| 8232f1ef65 | |||
| 78f5a8a6f8 | |||
| 3044aecbe9 | |||
| a5081ac307 | |||
| f396a4593e | |||
| aac9f972cf | |||
| aaa0944595 | |||
| 6cca127bbf | |||
| c1f3ce78e1 | |||
| 887b53b794 | |||
| a444df3fde | |||
| b038a1650e | |||
| 483556ac5b | |||
| 3e9e388745 | |||
| ab42acf4d7 | |||
| 0489ae53c4 | |||
| 35fcc299c0 | |||
| 568c549353 | |||
| edf130b341 | |||
| ed9e93c29f | |||
| 55f8ec8866 | |||
| 3e70154695 | |||
| aa17481c94 | |||
| b53bc24a63 | |||
| fbd0bf77c7 | |||
| 4da2156ebf | |||
| 8a67fcfee3 | |||
| 08f9793175 | |||
| a5b2fc9759 | |||
| 3fa8aff78e | |||
| 09ff9cb08e | |||
| c32300a386 | |||
| 55dc483c91 | |||
| 597ae2e716 | |||
| 50887e7e2c | |||
| 8743f23f13 | |||
| 72fe50bef6 | |||
| bae6333c26 | |||
| a08539d88d | |||
| bf7aecce90 | |||
| e2aa024a05 | |||
| edd5db296d | |||
| e14d6f11c6 | |||
| f3870a8a48 | |||
| 31bf5b8ff0 | |||
| a7ae456a06 | |||
| c03022efa3 | |||
| 46f5a65e68 | |||
| 44ec6b056e | |||
| 354007f265 | |||
| 6cb8a36cf1 | |||
| 435f253be8 | |||
| 0fe41ffb00 | |||
| bafa0cc3b8 | |||
| e23aa1ccf8 | |||
| a9db6b16eb | |||
| bf30f2e9e8 | |||
| ee9d50c0a5 | |||
| 6736534a52 | |||
| 4ccd819ec5 | |||
| 52b1e13aca | |||
| 67f3910f03 | |||
| 64cb3390ea | |||
| 0ac4a152be | |||
| 4e96e461f7 | |||
| 30bed8341a | |||
| d17733427b | |||
| 782d2a30cd | |||
| 84f30d9ef8 | |||
| ac49298c8d | |||
| a0256e1947 | |||
| 7bc2e1238d | |||
| 41f558b181 | |||
| 383b0914b3 | |||
| be297c4c7e | |||
| aa1f64bed6 | |||
| faf8bbcf13 | |||
| 0fa259089d | |||
| f7c7073cd7 | |||
| d7bf3920a5 | |||
| 7ee75d67c5 | |||
| d7db3aba36 | |||
| 7f99e99dad | |||
| fbea5b4cac | |||
| e6c88c05ad | |||
| 757813411d | |||
| 7dc05785cc | |||
| d7af43b87d | |||
| 6ea74ce740 | |||
| d5bdf7783e | |||
| 65d255a626 | |||
| 53a735a329 | |||
| 68713822fd | |||
| 3d26ac3323 | |||
| d487960ad8 | |||
| 9403cdd2a5 | |||
| 1fb1a32c9a | |||
| 31ddcc6278 | |||
| d789de9ea2 | |||
| e2014eb153 | |||
| 5932446508 | |||
| 61909e873f | |||
| 48cf7a4af9 | |||
| 9490cb4c8f | |||
| 86d4d10176 | |||
| 7b3b755aaf | |||
| 9afcbaed1d | |||
| 3a7cc9bb45 | |||
| ecd1da6525 | |||
| 9f2719bb1f | |||
| 85d0f2e861 | |||
| 5d007e636b | |||
| 7cd6f9038c | |||
| a80fd2f243 | |||
| 2487d27c45 | |||
| be7162a0df | |||
| c5a8372f13 | |||
| 81ca978413 | |||
| d6818c7015 | |||
| df5866dd95 | |||
| 34ee2b1ae9 | |||
| 8ca897da57 | |||
| c7fcd98cad | |||
| 8aa3124aa6 | |||
| b27ba9660b | |||
| 9f04b55572 | |||
| c4f4a9a158 | |||
| e2e8b43902 | |||
| 3a35642dc1 | |||
| 34a7aa2376 | |||
| 58eb32bce4 | |||
| c940d26f07 | |||
| fc5e25a07b | |||
| 1d32bced1c | |||
| f2a2d6bfa1 | |||
| 4f990ce488 | |||
| 106b7a9d8f | |||
| ef87d4dad4 | |||
| f6e46aecf5 | |||
| e9cf5f6f42 | |||
| e221c8a37d | |||
| b163544e3c | |||
| a718e92708 | |||
| 44d274e428 | |||
| c404fb7142 | |||
| 29c7987453 |
+32
-7
@@ -14,9 +14,15 @@ omit =
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
homeassistant/components/android_ip_webcam.py
|
||||
homeassistant/components/*/android_ip_webcam.py
|
||||
|
||||
homeassistant/components/bbb_gpio.py
|
||||
homeassistant/components/*/bbb_gpio.py
|
||||
|
||||
homeassistant/components/blink.py
|
||||
homeassistant/components/*/blink.py
|
||||
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
@@ -53,6 +59,9 @@ omit =
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
homeassistant/components/lutron_caseta.py
|
||||
homeassistant/components/*/lutron_caseta.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
@@ -85,6 +94,10 @@ omit =
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/twilio.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
homeassistant/components/vera.py
|
||||
homeassistant/components/*/vera.py
|
||||
|
||||
@@ -105,9 +118,6 @@ omit =
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zwave/*
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
@@ -132,6 +142,12 @@ omit =
|
||||
homeassistant/components/zabbix.py
|
||||
homeassistant/components/*/zabbix.py
|
||||
|
||||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
@@ -158,6 +174,7 @@ omit =
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/config/zwave.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/myq.py
|
||||
@@ -231,6 +248,7 @@ omit =
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/frontier_silicon.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/hdmi_cec.py
|
||||
@@ -255,10 +273,12 @@ omit =
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
@@ -286,8 +306,6 @@ omit =
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/telstra.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
@@ -303,16 +321,17 @@ omit =
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
@@ -333,11 +352,13 @@ omit =
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/kwb.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/lyft.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
@@ -411,7 +432,11 @@ omit =
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/__init__.py
|
||||
homeassistant/components/zwave/util.py
|
||||
homeassistant/components/zwave/workaround.py
|
||||
|
||||
|
||||
[report]
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
**Description:**
|
||||
## Description:
|
||||
|
||||
|
||||
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
|
||||
|
||||
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
|
||||
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
## Example entry for `configuration.yaml` (if applicable):
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
## Checklist:
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
|
||||
@@ -14,6 +14,8 @@ matrix:
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.6-dev"
|
||||
env: TOXENV=py36
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
include README.rst
|
||||
include LICENSE
|
||||
include LICENSE.md
|
||||
graft homeassistant
|
||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
recursive-exclude * *.py[co]
|
||||
|
||||
@@ -255,10 +255,13 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
||||
|
||||
def cmdline() -> List[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if sys.argv[0].endswith('/__main__.py'):
|
||||
if sys.argv[0].endswith(os.path.sep + '__main__.py'):
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
arg != '--daemon']
|
||||
else:
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
|
||||
+33
-394
@@ -4,320 +4,30 @@ import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from time import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.util.async import (
|
||||
run_coroutine_threadsafe, run_callback_threadsafe)
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
_PERSISTENT_ERRORS = {}
|
||||
HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)'
|
||||
|
||||
|
||||
def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_setup_component(hass, domain, config), loop=hass.loop).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if domain in hass.config.components:
|
||||
_LOGGER.debug('Component %s already set up.', domain)
|
||||
return True
|
||||
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
if config is None:
|
||||
config = {}
|
||||
|
||||
components = loader.load_order_component(domain)
|
||||
|
||||
# OrderedSet is empty if component or dependencies could not be resolved
|
||||
if not components:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
for component in components:
|
||||
res = yield from _async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error('Component %s failed to setup', component)
|
||||
_async_persistent_notification(hass, component, True)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_requirements(hass: core.HomeAssistant, component,
|
||||
name: str) -> bool:
|
||||
"""Install the requirements for a component.
|
||||
|
||||
This method needs to run in an executor.
|
||||
"""
|
||||
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
|
||||
return True
|
||||
|
||||
for req in component.REQUIREMENTS:
|
||||
if not pkg_util.install_package(req, target=hass.config.path('deps')):
|
||||
_LOGGER.error('Not initializing %s because could not install '
|
||||
'dependency %s', name, req)
|
||||
_async_persistent_notification(hass, name)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_setup_component(hass: core.HomeAssistant,
|
||||
domain: str, config) -> bool:
|
||||
"""Setup a component for Home Assistant.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
if domain in hass.config.components:
|
||||
return True
|
||||
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
setup_progress = hass.data.get('setup_progress')
|
||||
if setup_progress is None:
|
||||
setup_progress = hass.data['setup_progress'] = []
|
||||
|
||||
if domain in setup_progress:
|
||||
_LOGGER.error('Attempt made to setup %s during setup of %s',
|
||||
domain, domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
try:
|
||||
# Used to indicate to discovery that a setup is ongoing and allow it
|
||||
# to wait till it is done.
|
||||
did_lock = False
|
||||
if not setup_lock.locked():
|
||||
yield from setup_lock.acquire()
|
||||
did_lock = True
|
||||
|
||||
setup_progress.append(domain)
|
||||
config = yield from async_prepare_setup_component(hass, config, domain)
|
||||
|
||||
if config is None:
|
||||
return False
|
||||
|
||||
component = loader.get_component(domain)
|
||||
if component is None:
|
||||
_async_persistent_notification(hass, domain)
|
||||
return False
|
||||
|
||||
async_comp = hasattr(component, 'async_setup')
|
||||
|
||||
try:
|
||||
_LOGGER.info("Setting up %s", domain)
|
||||
if async_comp:
|
||||
result = yield from component.async_setup(hass, config)
|
||||
else:
|
||||
result = yield from hass.loop.run_in_executor(
|
||||
None, component.setup, hass, config)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error during setup of component %s', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
|
||||
if result is False:
|
||||
_LOGGER.error('component %s failed to initialize', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
return False
|
||||
elif result is not True:
|
||||
_LOGGER.error('component %s did not return boolean if setup '
|
||||
'was successful. Disabling component.', domain)
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
loader.set_component(domain, None)
|
||||
return False
|
||||
|
||||
hass.config.components.add(component.DOMAIN)
|
||||
|
||||
hass.bus.async_fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||
)
|
||||
|
||||
return True
|
||||
finally:
|
||||
setup_progress.remove(domain)
|
||||
if did_lock:
|
||||
setup_lock.release()
|
||||
|
||||
|
||||
def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_component(hass, config, domain), loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
domain: str):
|
||||
"""Prepare setup of a component and return processed config.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
component = loader.get_component(domain)
|
||||
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
|
||||
if dep not in hass.config.components]
|
||||
|
||||
if missing_deps:
|
||||
_LOGGER.error(
|
||||
'Not initializing %s because not all dependencies loaded: %s',
|
||||
domain, ", ".join(missing_deps))
|
||||
return None
|
||||
|
||||
if hasattr(component, 'CONFIG_SCHEMA'):
|
||||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
return None
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
platforms = []
|
||||
for p_name, p_config in config_per_platform(config, domain):
|
||||
# Validate component specific platform schema
|
||||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, domain, config, hass)
|
||||
continue
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
# So if p_name is None we are not going to validate platform
|
||||
# (the automation component is one of them)
|
||||
if p_name is None:
|
||||
platforms.append(p_validated)
|
||||
continue
|
||||
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, domain, p_name)
|
||||
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
# Validate platform specific schema
|
||||
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated, hass)
|
||||
continue
|
||||
|
||||
platforms.append(p_validated)
|
||||
|
||||
# Create a copy of the configuration with all config for current
|
||||
# component removed and add validated config back in.
|
||||
filter_keys = extract_domain_configs(config, domain)
|
||||
config = {key: value for key, value in config.items()
|
||||
if key not in filter_keys}
|
||||
config[domain] = platforms
|
||||
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, component, domain)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) -> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_prepare_setup_platform(hass, config, domain, platform_name),
|
||||
loop=hass.loop
|
||||
).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
platform_name: str) \
|
||||
-> Optional[ModuleType]:
|
||||
"""Load a platform and makes sure dependencies are setup.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
|
||||
|
||||
platform = loader.get_platform(domain, platform_name)
|
||||
|
||||
# Not found
|
||||
if platform is None:
|
||||
_LOGGER.error('Unable to find platform %s', platform_path)
|
||||
_async_persistent_notification(hass, platform_path)
|
||||
return None
|
||||
|
||||
# Already loaded
|
||||
elif platform_path in hass.config.components:
|
||||
return platform
|
||||
|
||||
# Load dependencies
|
||||
for component in getattr(platform, 'DEPENDENCIES', []):
|
||||
if component in loader.DEPENDENCY_BLACKLIST:
|
||||
raise HomeAssistantError(
|
||||
'{} is not allowed to be a dependency.'.format(component))
|
||||
|
||||
res = yield from async_setup_component(hass, component, config)
|
||||
if not res:
|
||||
_LOGGER.error(
|
||||
'Unable to prepare setup for platform %s because '
|
||||
'dependency %s could not be initialized', platform_path,
|
||||
component)
|
||||
_async_persistent_notification(hass, platform_path, True)
|
||||
return None
|
||||
|
||||
res = yield from hass.loop.run_in_executor(
|
||||
None, _handle_requirements, hass, platform, platform_path)
|
||||
if not res:
|
||||
return None
|
||||
|
||||
return platform
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction'))
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
@@ -339,23 +49,14 @@ def from_config_dict(config: Dict[str, Any],
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_dict(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.async_add_job(_async_init_from_config_dict(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
)
|
||||
|
||||
return future.result()
|
||||
return hass
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -372,19 +73,15 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
start = time()
|
||||
hass.async_track_tasks()
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
yield from setup_lock.acquire()
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
yield from conf_util.async_process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
yield from hass.loop.run_in_executor(
|
||||
@@ -429,24 +126,25 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
# stage 1
|
||||
for component in components:
|
||||
if component not in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
|
||||
# Setup the components
|
||||
dependency_blacklist = loader.DEPENDENCY_BLACKLIST - set(components)
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
for domain in loader.load_order_components(components):
|
||||
if domain in dependency_blacklist:
|
||||
raise HomeAssistantError(
|
||||
'{} is not allowed to be a dependency'.format(domain))
|
||||
|
||||
yield from _async_setup_component(hass, domain, config)
|
||||
|
||||
setup_lock.release()
|
||||
# stage 2
|
||||
for component in components:
|
||||
if component in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
|
||||
yield from hass.async_stop_track_tasks()
|
||||
|
||||
stop = time()
|
||||
_LOGGER.info('Home Assistant initialized in %ss', round(stop-start, 2))
|
||||
|
||||
async_register_signal_handling(hass)
|
||||
return hass
|
||||
|
||||
@@ -464,22 +162,13 @@ def from_config_file(config_path: str,
|
||||
if hass is None:
|
||||
hass = core.HomeAssistant()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_init_from_config_file(future):
|
||||
try:
|
||||
re_hass = yield from async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
future.set_result(re_hass)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.loop.create_task(_async_init_from_config_file(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
)
|
||||
|
||||
return future.result()
|
||||
return hass
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -504,7 +193,8 @@ def async_from_config_file(config_path: str,
|
||||
try:
|
||||
config_dict = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError:
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Error loading %s: %s', config_path, err)
|
||||
return None
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
@@ -588,57 +278,6 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
'Unable to setup error log %s (access denied)', err_log_path)
|
||||
|
||||
|
||||
def log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_log_exception, ex, domain, config, hass).result()
|
||||
|
||||
|
||||
@core.callback
|
||||
def _async_persistent_notification(hass: core.HomeAssistant, component: str,
|
||||
link: Optional[bool]=False):
|
||||
"""Print a persistent notification.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
_PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link
|
||||
_lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name)
|
||||
if link else name for name, link in _PERSISTENT_ERRORS.items()]
|
||||
message = ('The following components and platforms could not be set up:\n'
|
||||
'* ' + '\n* '.join(list(_lst)) + '\nPlease check your config')
|
||||
persistent_notification.async_create(
|
||||
hass, message, 'Invalid config', 'invalid_config')
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_log_exception(ex, domain, config, hass):
|
||||
"""Generate log exception for config validation.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
if hass is not None:
|
||||
_async_persistent_notification(hass, domain, True)
|
||||
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||
.format(ex.path[-1], domain, domain,
|
||||
'->'.join(str(m) for m in ex.path))
|
||||
else:
|
||||
message += '{}.'.format(humanize_error(config, ex))
|
||||
|
||||
domain_config = config.get(domain, config)
|
||||
message += " (See {}, line {}). ".format(
|
||||
getattr(domain_config, '__config_file__', '?'),
|
||||
getattr(domain_config, '__line__', '?'))
|
||||
|
||||
if domain != 'homeassistant':
|
||||
message += ('Please check the docs at '
|
||||
'https://home-assistant.io/components/{}/'.format(domain))
|
||||
|
||||
_LOGGER.error(message)
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ def async_setup(hass, config):
|
||||
if not alarm.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
alarm.async_update_ha_state(True))
|
||||
if hasattr(alarm, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
@@ -55,7 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
@callback
|
||||
def alarm_keypress_handler(service):
|
||||
@@ -94,10 +94,13 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self, partition):
|
||||
|
||||
@@ -46,7 +46,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the MQTT platform."""
|
||||
yield from async_add_devices([MqttAlarm(
|
||||
async_add_devices([MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
||||
@@ -18,7 +18,6 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers import service, event
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -62,8 +61,7 @@ def is_on(hass, entity_id):
|
||||
|
||||
def turn_on(hass, entity_id):
|
||||
"""Reset the alert."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_turn_on, hass, entity_id).result()
|
||||
hass.add_job(async_turn_on, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -76,8 +74,7 @@ def async_turn_on(hass, entity_id):
|
||||
|
||||
def turn_off(hass, entity_id):
|
||||
"""Acknowledge alert."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_turn_off, hass, entity_id).result()
|
||||
hass.add_job(async_turn_off, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -90,7 +87,7 @@ def async_turn_off(hass, entity_id):
|
||||
|
||||
def toggle(hass, entity_id):
|
||||
"""Toggle acknowledgement of alert."""
|
||||
run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id)
|
||||
hass.add_job(async_toggle, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
Support for IP Webcam, an Android app that acts as a full-featured webcam.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/android_ip_webcam/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL,
|
||||
CONF_PLATFORM)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send, async_dispatcher_connect)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
||||
|
||||
DOMAIN = 'android_ip_webcam'
|
||||
REQUIREMENTS = ["pydroid-ipcam==0.6"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
DATA_IP_WEBCAM = 'android_ip_webcam'
|
||||
|
||||
ATTR_HOST = 'host'
|
||||
ATTR_VID_CONNS = 'Video Connections'
|
||||
ATTR_AUD_CONNS = 'Audio Connections'
|
||||
|
||||
KEY_MAP = {
|
||||
'audio_connections': 'Audio Connections',
|
||||
'adet_limit': 'Audio Trigger Limit',
|
||||
'antibanding': 'Anti-banding',
|
||||
'audio_only': 'Audio Only',
|
||||
'battery_level': 'Battery Level',
|
||||
'battery_temp': 'Battery Temperature',
|
||||
'battery_voltage': 'Battery Voltage',
|
||||
'coloreffect': 'Color Effect',
|
||||
'exposure': 'Exposure Level',
|
||||
'exposure_lock': 'Exposure Lock',
|
||||
'ffc': 'Front-facing Camera',
|
||||
'flashmode': 'Flash Mode',
|
||||
'focus': 'Focus',
|
||||
'focus_homing': 'Focus Homing',
|
||||
'focus_region': 'Focus Region',
|
||||
'focusmode': 'Focus Mode',
|
||||
'gps_active': 'GPS Active',
|
||||
'idle': 'Idle',
|
||||
'ip_address': 'IPv4 Address',
|
||||
'ipv6_address': 'IPv6 Address',
|
||||
'ivideon_streaming': 'Ivideon Streaming',
|
||||
'light': 'Light Level',
|
||||
'mirror_flip': 'Mirror Flip',
|
||||
'motion': 'Motion',
|
||||
'motion_active': 'Motion Active',
|
||||
'motion_detect': 'Motion Detection',
|
||||
'motion_event': 'Motion Event',
|
||||
'motion_limit': 'Motion Limit',
|
||||
'night_vision': 'Night Vision',
|
||||
'night_vision_average': 'Night Vision Average',
|
||||
'night_vision_gain': 'Night Vision Gain',
|
||||
'orientation': 'Orientation',
|
||||
'overlay': 'Overlay',
|
||||
'photo_size': 'Photo Size',
|
||||
'pressure': 'Pressure',
|
||||
'proximity': 'Proximity',
|
||||
'quality': 'Quality',
|
||||
'scenemode': 'Scene Mode',
|
||||
'sound': 'Sound',
|
||||
'sound_event': 'Sound Event',
|
||||
'sound_timeout': 'Sound Timeout',
|
||||
'torch': 'Torch',
|
||||
'video_connections': 'Video Connections',
|
||||
'video_chunk_len': 'Video Chunk Length',
|
||||
'video_recording': 'Video Recording',
|
||||
'video_size': 'Video Size',
|
||||
'whitebalance': 'White Balance',
|
||||
'whitebalance_lock': 'White Balance Lock',
|
||||
'zoom': 'Zoom'
|
||||
}
|
||||
|
||||
ICON_MAP = {
|
||||
'audio_connections': 'mdi:speaker',
|
||||
'battery_level': 'mdi:battery',
|
||||
'battery_temp': 'mdi:thermometer',
|
||||
'battery_voltage': 'mdi:battery-charging-100',
|
||||
'exposure_lock': 'mdi:camera',
|
||||
'ffc': 'mdi:camera-front-variant',
|
||||
'focus': 'mdi:image-filter-center-focus',
|
||||
'gps_active': 'mdi:crosshairs-gps',
|
||||
'light': 'mdi:flashlight',
|
||||
'motion': 'mdi:run',
|
||||
'night_vision': 'mdi:weather-night',
|
||||
'overlay': 'mdi:monitor',
|
||||
'pressure': 'mdi:gauge',
|
||||
'proximity': 'mdi:map-marker-radius',
|
||||
'quality': 'mdi:quality-high',
|
||||
'sound': 'mdi:speaker',
|
||||
'sound_event': 'mdi:speaker',
|
||||
'sound_timeout': 'mdi:speaker',
|
||||
'torch': 'mdi:white-balance-sunny',
|
||||
'video_chunk_len': 'mdi:video',
|
||||
'video_connections': 'mdi:eye',
|
||||
'video_recording': 'mdi:record-rec',
|
||||
'whitebalance_lock': 'mdi:white-balance-auto'
|
||||
}
|
||||
|
||||
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision',
|
||||
'overlay', 'torch', 'whitebalance_lock', 'video_recording']
|
||||
|
||||
SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
||||
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
|
||||
'sound', 'video_connections']
|
||||
|
||||
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
||||
|
||||
CONF_MOTION_SENSOR = 'motion_sensor'
|
||||
|
||||
DEFAULT_NAME = 'IP Webcam'
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_SWITCHES, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(CONF_MOTION_SENSOR, default=None): cv.boolean,
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the IP Webcam component."""
|
||||
from pydroid_ipcam import PyDroidIPCam
|
||||
|
||||
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_ipcamera(cam_config):
|
||||
"""Setup a ip camera."""
|
||||
host = cam_config[CONF_HOST]
|
||||
username = cam_config.get(CONF_USERNAME)
|
||||
password = cam_config.get(CONF_PASSWORD)
|
||||
name = cam_config[CONF_NAME]
|
||||
interval = cam_config[CONF_SCAN_INTERVAL]
|
||||
switches = cam_config[CONF_SWITCHES]
|
||||
sensors = cam_config[CONF_SENSORS]
|
||||
motion = cam_config[CONF_MOTION_SENSOR]
|
||||
|
||||
# init ip webcam
|
||||
cam = PyDroidIPCam(
|
||||
hass.loop, websession, host, cam_config[CONF_PORT],
|
||||
username=username, password=password,
|
||||
timeout=cam_config[CONF_TIMEOUT]
|
||||
)
|
||||
|
||||
if switches is None:
|
||||
switches = [setting for setting in cam.enabled_settings
|
||||
if setting in SWITCHES]
|
||||
|
||||
if sensors is None:
|
||||
sensors = [sensor for sensor in cam.enabled_sensors
|
||||
if sensor in SENSORS]
|
||||
sensors.extend(['audio_connections', 'video_connections'])
|
||||
|
||||
if motion is None:
|
||||
motion = 'motion_active' in cam.enabled_sensors
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_data(now):
|
||||
"""Update data from ipcam in SCAN_INTERVAL."""
|
||||
yield from cam.update()
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
||||
|
||||
async_track_point_in_utc_time(
|
||||
hass, async_update_data, utcnow() + interval)
|
||||
|
||||
yield from async_update_data(None)
|
||||
|
||||
# load platforms
|
||||
webcams[host] = cam
|
||||
|
||||
mjpeg_camera = {
|
||||
CONF_PLATFORM: 'mjpeg',
|
||||
CONF_MJPEG_URL: cam.mjpeg_url,
|
||||
CONF_STILL_IMAGE_URL: cam.image_url,
|
||||
CONF_NAME: name,
|
||||
}
|
||||
if username and password:
|
||||
mjpeg_camera.update({
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password
|
||||
})
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'camera', 'mjpeg', mjpeg_camera, config))
|
||||
|
||||
if sensors:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config))
|
||||
|
||||
if switches:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
CONF_SWITCHES: switches,
|
||||
}, config))
|
||||
|
||||
if motion:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
CONF_HOST: host,
|
||||
CONF_NAME: name,
|
||||
}, config))
|
||||
|
||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AndroidIPCamEntity(Entity):
|
||||
"""The Android device running IP Webcam."""
|
||||
|
||||
def __init__(self, host, ipcam):
|
||||
"""Initialize the data oject."""
|
||||
self._host = host
|
||||
self._ipcam = ipcam
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register update dispatcher."""
|
||||
@callback
|
||||
def async_ipcam_update(host):
|
||||
"""Update callback."""
|
||||
if self._host != host:
|
||||
return
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._ipcam.available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {ATTR_HOST: self._host}
|
||||
if self._ipcam.status_data is None:
|
||||
return state_attr
|
||||
|
||||
state_attr[ATTR_VID_CONNS] = \
|
||||
self._ipcam.status_data.get('video_connections')
|
||||
state_attr[ATTR_AUD_CONNS] = \
|
||||
self._ipcam.status_data.get('audio_connections')
|
||||
|
||||
return state_attr
|
||||
@@ -327,6 +327,8 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
_LOGGER.warning('Event forwarding is deprecated. '
|
||||
'Will be removed by 0.43')
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
|
||||
@@ -11,16 +11,17 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import async_prepare_setup_platform
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE)
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD)
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.loader import get_platform
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -28,8 +29,6 @@ import homeassistant.helpers.config_validation as cv
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DEPENDENCIES = ['group']
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
@@ -52,7 +51,6 @@ DEFAULT_INITIAL_STATE = True
|
||||
ATTR_LAST_TRIGGERED = 'last_triggered'
|
||||
ATTR_VARIABLES = 'variables'
|
||||
SERVICE_TRIGGER = 'trigger'
|
||||
SERVICE_RELOAD = 'reload'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -226,7 +224,7 @@ class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||
hidden):
|
||||
hidden, initial_state):
|
||||
"""Initialize an automation entity."""
|
||||
self._name = name
|
||||
self._async_attach_triggers = async_attach_triggers
|
||||
@@ -236,6 +234,7 @@ class AutomationEntity(ToggleEntity):
|
||||
self._enabled = False
|
||||
self._last_triggered = None
|
||||
self._hidden = hidden
|
||||
self._initial_state = initial_state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -264,6 +263,18 @@ class AutomationEntity(ToggleEntity):
|
||||
"""Return True if entity is on."""
|
||||
return self._enabled
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
"""Startup with initial state or previous state."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state is None:
|
||||
if self._initial_state:
|
||||
yield from self.async_enable()
|
||||
else:
|
||||
self._last_triggered = state.attributes.get('last_triggered')
|
||||
if state.state == STATE_ON:
|
||||
yield from self.async_enable()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on and update the state."""
|
||||
@@ -322,7 +333,6 @@ def _async_process_config(hass, config, component):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
entities = []
|
||||
tasks = []
|
||||
|
||||
for config_key in extract_domain_configs(config, DOMAIN):
|
||||
conf = config[config_key]
|
||||
@@ -332,6 +342,7 @@ def _async_process_config(hass, config, component):
|
||||
list_no)
|
||||
|
||||
hidden = config_block[CONF_HIDE_ENTITY]
|
||||
initial_state = config_block[CONF_INITIAL_STATE]
|
||||
|
||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
||||
name)
|
||||
@@ -348,15 +359,14 @@ def _async_process_config(hass, config, component):
|
||||
|
||||
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(entity.async_enable())
|
||||
config_block.get(CONF_TRIGGER, []), name
|
||||
)
|
||||
entity = AutomationEntity(
|
||||
name, async_attach_triggers, cond_func, action, hidden,
|
||||
initial_state)
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Support for IP Webcam binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.android_ip_webcam/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.android_ip_webcam import (
|
||||
KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME)
|
||||
|
||||
DEPENDENCIES = ['android_ip_webcam']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup IP Webcam binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
host = discovery_info[CONF_HOST]
|
||||
name = discovery_info[CONF_NAME]
|
||||
ipcam = hass.data[DATA_IP_WEBCAM][host]
|
||||
|
||||
async_add_devices(
|
||||
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)
|
||||
|
||||
|
||||
class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
||||
"""Represents an IP Webcam binary sensor."""
|
||||
|
||||
def __init__(self, name, host, ipcam, sensor):
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(host, ipcam)
|
||||
|
||||
self._sensor = sensor
|
||||
self._mapped_name = KEY_MAP.get(self._sensor, self._sensor)
|
||||
self._name = '{} {}'.format(name, self._mapped_name)
|
||||
self._state = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor, if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
state, _ = self._ipcam.export_sensor(self._sensor)
|
||||
self._state = state == 1.0
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'motion'
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Support for Blink system camera control.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.blink/
|
||||
"""
|
||||
from homeassistant.components.blink import DOMAIN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the blink binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[DOMAIN].blink
|
||||
devs = list()
|
||||
for name in data.cameras:
|
||||
devs.append(BlinkCameraMotionSensor(name, data))
|
||||
devs.append(BlinkSystemSensor(data))
|
||||
add_devices(devs, True)
|
||||
|
||||
|
||||
class BlinkCameraMotionSensor(BinarySensorDevice):
|
||||
"""A representation of a Blink binary sensor."""
|
||||
|
||||
def __init__(self, name, data):
|
||||
"""Initialize the sensor."""
|
||||
self._name = 'blink_' + name + '_motion_enabled'
|
||||
self._camera_name = name
|
||||
self.data = data
|
||||
self._state = self.data.cameras[self._camera_name].armed
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blink sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update sensor state."""
|
||||
self.data.refresh()
|
||||
self._state = self.data.cameras[self._camera_name].armed
|
||||
|
||||
|
||||
class BlinkSystemSensor(BinarySensorDevice):
|
||||
"""A representation of a Blink system sensor."""
|
||||
|
||||
def __init__(self, data):
|
||||
"""Initialize the sensor."""
|
||||
self._name = 'blink armed status'
|
||||
self.data = data
|
||||
self._state = self.data.arm
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blink sensor."""
|
||||
return self._name.replace(" ", "_")
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update sensor state."""
|
||||
self.data.refresh()
|
||||
self._state = self.data.arm
|
||||
@@ -36,6 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
dev = []
|
||||
for droplet in droplets:
|
||||
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
|
||||
if droplet_id is None:
|
||||
_LOGGER.error("Droplet %s is not available", droplet)
|
||||
return False
|
||||
dev.append(DigitalOceanBinarySensor(
|
||||
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
This method is called when there is an incoming packet associated
|
||||
with this platform.
|
||||
"""
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
if value2 == 0x70:
|
||||
self.which = 0
|
||||
self.onoff = 0
|
||||
|
||||
@@ -37,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
@@ -52,8 +52,11 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
super().__init__(zone_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
||||
@@ -57,16 +57,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegMotion(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Constructor for binary sensor noise detection."""
|
||||
super().__init__(config.get(CONF_INITIAL_STATE))
|
||||
|
||||
@@ -98,15 +95,19 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||
"""Initialize ffmpeg motion binary sensor."""
|
||||
from haffmpeg import SensorMotion
|
||||
|
||||
super().__init__(hass, config)
|
||||
super().__init__(config)
|
||||
self.ffmpeg = SensorMotion(
|
||||
manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_start_ffmpeg(self, entity_ids):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if entity_ids is not None and self.entity_id not in entity_ids:
|
||||
return
|
||||
|
||||
# init config
|
||||
self.ffmpeg.set_options(
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
@@ -116,7 +117,7 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
yield from self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@@ -54,10 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegNoise(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegNoise(FFmpegBinarySensor):
|
||||
@@ -67,15 +64,19 @@ class FFmpegNoise(FFmpegBinarySensor):
|
||||
"""Initialize ffmpeg noise binary sensor."""
|
||||
from haffmpeg import SensorNoise
|
||||
|
||||
super().__init__(hass, config)
|
||||
super().__init__(config)
|
||||
self.ffmpeg = SensorNoise(
|
||||
manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_start_ffmpeg(self, entity_ids):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if entity_ids is not None and self.entity_id not in entity_ids:
|
||||
return
|
||||
|
||||
# init config
|
||||
self.ffmpeg.set_options(
|
||||
time_duration=self._config.get(CONF_DURATION),
|
||||
@@ -84,7 +85,7 @@ class FFmpegNoise(FFmpegBinarySensor):
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
yield from self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
output_dest=self._config.get(CONF_OUTPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
|
||||
@@ -15,9 +15,10 @@ from homeassistant.components.binary_sensor import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.0.7', 'pydispatcher==2.0.5']
|
||||
REQUIREMENTS = ['pyhik==0.1.0']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
@@ -119,30 +120,32 @@ class HikvisionData(object):
|
||||
self._password = password
|
||||
|
||||
# Establish camera
|
||||
self._cam = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
self.camdata = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
|
||||
if self._name is None:
|
||||
self._name = self._cam.get_name
|
||||
|
||||
# Start event stream
|
||||
self._cam.start_stream()
|
||||
self._name = self.camdata.get_name
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_hik)
|
||||
|
||||
def stop_hik(self, event):
|
||||
"""Shutdown Hikvision subscriptions and subscription thread on exit."""
|
||||
self._cam.disconnect()
|
||||
self.camdata.disconnect()
|
||||
|
||||
def start_hik(self, event):
|
||||
"""Start Hikvision event stream thread."""
|
||||
self.camdata.start_stream()
|
||||
|
||||
@property
|
||||
def sensors(self):
|
||||
"""Return list of available sensors and their states."""
|
||||
return self._cam.current_event_states
|
||||
return self.camdata.current_event_states
|
||||
|
||||
@property
|
||||
def cam_id(self):
|
||||
"""Return camera id."""
|
||||
return self._cam.get_id
|
||||
return self.camdata.get_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -155,8 +158,6 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, hass, sensor, cam, delay):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
|
||||
self._hass = hass
|
||||
self._cam = cam
|
||||
self._name = self._cam.name + ' ' + sensor
|
||||
@@ -170,12 +171,8 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
self._timer = None
|
||||
|
||||
# Form signal for dispatcher
|
||||
signal = 'ValueChanged.{}'.format(self._cam.cam_id)
|
||||
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=signal,
|
||||
sender=self._sensor)
|
||||
# Register callback function with pyHik
|
||||
self._cam.camdata.add_update_callback(self._update_callback, self._id)
|
||||
|
||||
def _sensor_state(self):
|
||||
"""Extract sensor state."""
|
||||
@@ -225,13 +222,9 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
return attr
|
||||
|
||||
def _update_callback(self, signal, sender):
|
||||
def _update_callback(self, msg):
|
||||
"""Update the sensor's state, if needed."""
|
||||
_LOGGER.debug('Dispatcher callback, signal: %s, sender: %s',
|
||||
signal, sender)
|
||||
|
||||
if sender is not self._sensor:
|
||||
return
|
||||
_LOGGER.debug('Callback signal from: %s', msg)
|
||||
|
||||
if self._delay > 0 and not self.is_on:
|
||||
# Set timer to wait until updating the state
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Support for MAX! Window Shutter via MAX! Cube.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/maxcube/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add window shutters to HASS."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
|
||||
# List of devices
|
||||
devices = []
|
||||
|
||||
for device in cube.devices:
|
||||
# Create device name by concatenating room name + device name
|
||||
name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
# Only add Window Shutters
|
||||
if cube.is_windowshutter(device):
|
||||
# add device to HASS
|
||||
devices.append(MaxCubeShutter(hass, name, device.rf_address))
|
||||
|
||||
if len(devices) > 0:
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class MaxCubeShutter(BinarySensorDevice):
|
||||
"""MAX! Cube BinarySensor device."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
"""Initialize MAX! Cube BinarySensorDevice."""
|
||||
self._name = name
|
||||
self._sensor_type = 'opening'
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is required."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the BinarySensorDevice."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on/open."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get latest data from MAX! Cube."""
|
||||
self._cubehandle.update()
|
||||
|
||||
# Get the device we want to update
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Update our internal state
|
||||
self._state = device.is_open
|
||||
@@ -46,7 +46,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
yield from async_add_devices([MqttBinarySensor(
|
||||
async_add_devices([MqttBinarySensor(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS),
|
||||
|
||||
@@ -15,12 +15,14 @@ from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS)
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS,
|
||||
EVENT_HOMEASSISTANT_START, STATE_ON)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -66,7 +68,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
|
||||
yield from async_add_devices(sensors, True)
|
||||
async_add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
@@ -83,14 +85,30 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
self._device_class = device_class
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
self._entities = entity_ids
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._state = state.state == STATE_ON
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
async_track_state_change(
|
||||
hass, entity_ids, template_bsensor_state_listener)
|
||||
@callback
|
||||
def template_bsensor_startup(event):
|
||||
"""Update template on startup."""
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_bsensor_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -52,7 +52,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
limit_type = config.get(CONF_TYPE)
|
||||
device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
|
||||
yield from async_add_devices(
|
||||
async_add_devices(
|
||||
[ThresholdSensor(hass, entity_id, name, threshold, limit_type,
|
||||
device_class)], True)
|
||||
return True
|
||||
|
||||
@@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice)
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@@ -30,6 +30,7 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._state = False
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Sensor to indicate whether the current day is a workday."""
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
CONF_NAME, WEEKDAYS)
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.8.1']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Canada', 'CA',
|
||||
'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'England',
|
||||
'EuropeanCentralBank', 'ECB', 'TAR', 'Germany', 'DE',
|
||||
'Ireland', 'Isle of Man', 'Mexico', 'MX', 'Netherlands', 'NL',
|
||||
'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO',
|
||||
'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Spain',
|
||||
'ES', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales']
|
||||
CONF_COUNTRY = 'country'
|
||||
CONF_PROVINCE = 'province'
|
||||
CONF_WORKDAYS = 'workdays'
|
||||
# By default, Monday - Friday are workdays
|
||||
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
|
||||
CONF_EXCLUDES = 'excludes'
|
||||
# By default, public holidays, Saturdays and Sundays are excluded from workdays
|
||||
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
|
||||
DEFAULT_NAME = 'Workday Sensor'
|
||||
ALLOWED_DAYS = WEEKDAYS + ['holiday']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
|
||||
vol.Optional(CONF_PROVINCE, default=None): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Workday sensor."""
|
||||
import holidays
|
||||
|
||||
# Get the Sensor name from the config
|
||||
sensor_name = config.get(CONF_NAME)
|
||||
|
||||
# Get the country code from the config
|
||||
country = config.get(CONF_COUNTRY)
|
||||
|
||||
# Get the province from the config
|
||||
province = config.get(CONF_PROVINCE)
|
||||
|
||||
# Get the list of workdays from the config
|
||||
workdays = config.get(CONF_WORKDAYS)
|
||||
|
||||
# Get the list of excludes from the config
|
||||
excludes = config.get(CONF_EXCLUDES)
|
||||
|
||||
# Instantiate the holidays module for the current year
|
||||
year = datetime.datetime.now().year
|
||||
obj_holidays = getattr(holidays, country)(years=year)
|
||||
|
||||
# Also apply the provience, if available for the configured country
|
||||
if province:
|
||||
if province not in obj_holidays.PROVINCES:
|
||||
_LOGGER.error('There is no province/state %s in country %s',
|
||||
province, country)
|
||||
return False
|
||||
else:
|
||||
year = datetime.datetime.now().year
|
||||
obj_holidays = getattr(holidays, country)(prov=province,
|
||||
years=year)
|
||||
|
||||
# Output found public holidays via the debug channel
|
||||
_LOGGER.debug("Found the following holidays for your configuration:")
|
||||
for date, name in sorted(obj_holidays.items()):
|
||||
_LOGGER.debug("%s %s", date, name)
|
||||
|
||||
# Add ourselves as device
|
||||
add_devices([IsWorkdaySensor(obj_holidays, workdays,
|
||||
excludes, sensor_name)], True)
|
||||
|
||||
|
||||
def day_to_string(day):
|
||||
"""Convert day index 0 - 7 to string."""
|
||||
try:
|
||||
return ALLOWED_DAYS[day]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
class IsWorkdaySensor(Entity):
|
||||
"""Implementation of a Workday sensor."""
|
||||
|
||||
def __init__(self, obj_holidays, workdays, excludes, name):
|
||||
"""Initialize the Workday sensor."""
|
||||
self._name = name
|
||||
self._obj_holidays = obj_holidays
|
||||
self._workdays = workdays
|
||||
self._excludes = excludes
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def is_include(self, day, now):
|
||||
"""Check if given day is in the includes list."""
|
||||
# Check includes
|
||||
if day in self._workdays:
|
||||
return True
|
||||
elif 'holiday' in self._workdays and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_exclude(self, day, now):
|
||||
"""Check if given day is in the excludes list."""
|
||||
# Check excludes
|
||||
if day in self._excludes:
|
||||
return True
|
||||
elif 'holiday' in self._excludes and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get date and look whether it is a holiday."""
|
||||
# Default is no workday
|
||||
self._state = STATE_OFF
|
||||
|
||||
# Get iso day of the week (1 = Monday, 7 = Sunday)
|
||||
day = datetime.datetime.today().isoweekday() - 1
|
||||
day_of_week = day_to_string(day)
|
||||
|
||||
if self.is_include(day_of_week, dt_util.now()):
|
||||
self._state = STATE_ON
|
||||
|
||||
if self.is_exclude(day_of_week, dt_util.now()):
|
||||
self._state = STATE_OFF
|
||||
@@ -24,7 +24,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the ZigBee binary sensor platform."""
|
||||
add_devices([ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))])
|
||||
add_devices(
|
||||
[ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))], True)
|
||||
|
||||
|
||||
class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice):
|
||||
|
||||
@@ -10,6 +10,7 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import workaround
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
BinarySensorDevice)
|
||||
@@ -18,45 +19,34 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Z-Wave platform for binary sensors."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
device_mapping = workaround.get_device_mapping(value)
|
||||
def get_device(values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
device_mapping = workaround.get_device_mapping(values.primary)
|
||||
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
|
||||
# Default the multiplier to 4
|
||||
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
|
||||
add_devices([
|
||||
ZWaveTriggerSensor(value, "motion",
|
||||
hass, re_arm_multiplier * 8)
|
||||
])
|
||||
return
|
||||
re_arm_multiplier = zwave.get_config_value(values.primary.node, 9) or 4
|
||||
return ZWaveTriggerSensor(values, "motion", re_arm_multiplier * 8)
|
||||
|
||||
if workaround.get_device_component_mapping(value) == DOMAIN:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
return
|
||||
if workaround.get_device_component_mapping(values.primary) == DOMAIN:
|
||||
return ZWaveBinarySensor(values, None)
|
||||
|
||||
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
if values.primary.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
return ZWaveBinarySensor(values, None)
|
||||
return None
|
||||
|
||||
|
||||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
"""Representation of a binary sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, device_class):
|
||||
def __init__(self, values, device_class):
|
||||
"""Initialize the sensor."""
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._sensor_type = device_class
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -68,35 +58,27 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
"""Representation of a stateless sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, device_class, hass, re_arm_sec=60):
|
||||
def __init__(self, values, device_class, re_arm_sec=60):
|
||||
"""Initialize the sensor."""
|
||||
super(ZWaveTriggerSensor, self).__init__(value, device_class)
|
||||
self._hass = hass
|
||||
super(ZWaveTriggerSensor, self).__init__(values, device_class)
|
||||
self.re_arm_sec = re_arm_sec
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
# If it's active make sure that we set the timeout tracker
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
self.invalidate_after = None
|
||||
|
||||
def update_properties(self):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
# only allow this value to be true for re_arm secs
|
||||
if not self.hass:
|
||||
return
|
||||
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Support for Blink Home Camera System.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/blink/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_ARMED)
|
||||
from homeassistant.helpers import discovery
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'blink'
|
||||
REQUIREMENTS = ['blinkpy==0.5.2']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ARM_SYSTEM_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ARMED): cv.boolean
|
||||
})
|
||||
|
||||
ARM_CAMERA_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(ATTR_ARMED): cv.boolean
|
||||
})
|
||||
|
||||
SNAP_PICTURE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_FRIENDLY_NAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
class BlinkSystem(object):
|
||||
"""Blink System class."""
|
||||
|
||||
def __init__(self, config_info):
|
||||
"""Initialize the system."""
|
||||
import blinkpy
|
||||
self.blink = blinkpy.Blink(username=config_info[DOMAIN][CONF_USERNAME],
|
||||
password=config_info[DOMAIN][CONF_PASSWORD])
|
||||
self.blink.setup_system()
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup Blink System."""
|
||||
hass.data[DOMAIN] = BlinkSystem(config)
|
||||
discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
||||
|
||||
def snap_picture(call):
|
||||
"""Take a picture."""
|
||||
cameras = hass.data[DOMAIN].blink.cameras
|
||||
name = call.data.get(ATTR_FRIENDLY_NAME, '')
|
||||
if name in cameras:
|
||||
cameras[name].snap_picture()
|
||||
|
||||
def arm_camera(call):
|
||||
"""Arm a camera."""
|
||||
cameras = hass.data[DOMAIN].blink.cameras
|
||||
name = call.data.get(ATTR_FRIENDLY_NAME, '')
|
||||
value = call.data.get(ATTR_ARMED, True)
|
||||
if name in cameras:
|
||||
cameras[name].set_motion_detect(value)
|
||||
|
||||
def arm_system(call):
|
||||
"""Arm the system."""
|
||||
value = call.data.get(ATTR_ARMED, True)
|
||||
hass.data[DOMAIN].blink.arm = value
|
||||
hass.data[DOMAIN].blink.refresh()
|
||||
|
||||
hass.services.register(DOMAIN, 'snap_picture', snap_picture,
|
||||
schema=SNAP_PICTURE_SCHEMA)
|
||||
hass.services.register(DOMAIN, 'arm_camera', arm_camera,
|
||||
schema=ARM_CAMERA_SCHEMA)
|
||||
hass.services.register(DOMAIN, 'arm_system', arm_system,
|
||||
schema=ARM_SYSTEM_SCHEMA)
|
||||
|
||||
return True
|
||||
@@ -3,8 +3,8 @@ Support for Google Calendar event device sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -27,13 +27,13 @@ DOMAIN = 'calendar'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ class CalendarEventDevice(Entity):
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
||||
summary = self.data.event['summary']
|
||||
summary = self.data.event.get('summary', '')
|
||||
|
||||
# check if we have an offset tag in the message
|
||||
# time is HH:MM or MM
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Support for Blink system camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.blink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
|
||||
from homeassistant.components.blink import DOMAIN
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup a Blink Camera."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[DOMAIN].blink
|
||||
devs = list()
|
||||
for name in data.cameras:
|
||||
devs.append(BlinkCamera(hass, config, data, name))
|
||||
|
||||
add_devices(devs)
|
||||
|
||||
|
||||
class BlinkCamera(Camera):
|
||||
"""An implementation of a Blink Camera."""
|
||||
|
||||
def __init__(self, hass, config, data, name):
|
||||
"""Initialize a camera."""
|
||||
super().__init__()
|
||||
self.data = data
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.notifications = self.data.cameras[self._name].notifications
|
||||
self.response = None
|
||||
|
||||
_LOGGER.info("Initialized blink camera %s", self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""A camera name."""
|
||||
return self._name
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def request_image(self):
|
||||
"""An image request from Blink servers."""
|
||||
_LOGGER.info("Requesting new image from blink servers")
|
||||
image_url = self.check_for_motion()
|
||||
header = self.data.cameras[self._name].header
|
||||
self.response = requests.get(image_url, headers=header, stream=True)
|
||||
|
||||
def check_for_motion(self):
|
||||
"""A method to check if motion has been detected since last update."""
|
||||
self.data.refresh()
|
||||
notifs = self.data.cameras[self._name].notifications
|
||||
if notifs > self.notifications:
|
||||
# We detected motion at some point
|
||||
self.data.last_motion()
|
||||
self.notifications = notifs
|
||||
# returning motion image currently not working
|
||||
# return self.data.cameras[self._name].motion['image']
|
||||
elif notifs < self.notifications:
|
||||
self.notifications = notifs
|
||||
|
||||
return self.data.camera_thumbs[self._name]
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
self.request_image()
|
||||
return self.response.content
|
||||
@@ -34,7 +34,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
yield from async_add_devices([FFmpegCamera(hass, config)])
|
||||
async_add_devices([FFmpegCamera(hass, config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
|
||||
@@ -47,15 +47,21 @@ class FoscamCamera(Camera):
|
||||
port = device_info.get(CONF_PORT)
|
||||
|
||||
self._base_url = 'http://{}:{}/'.format(ip_address, port)
|
||||
|
||||
uri_template = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?' \
|
||||
+ 'cmd=snapPicture2&usr={}&pwd={}'
|
||||
|
||||
self._username = device_info.get(CONF_USERNAME)
|
||||
self._password = device_info.get(CONF_PASSWORD)
|
||||
self._snap_picture_url = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \
|
||||
+ self._username + '&pwd=' + self._password
|
||||
self._snap_picture_url = uri_template.format(
|
||||
self._username,
|
||||
self._password
|
||||
)
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
|
||||
_LOGGER.info('Using the following URL for %s: %s',
|
||||
self._name, self._snap_picture_url)
|
||||
self._name, uri_template.format('***', '***'))
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
|
||||
@@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a generic IP Camera."""
|
||||
yield from async_add_devices([GenericCamera(hass, config)])
|
||||
async_add_devices([GenericCamera(hass, config)])
|
||||
|
||||
|
||||
class GenericCamera(Camera):
|
||||
|
||||
@@ -20,7 +20,7 @@ CONF_FILE_PATH = 'file_path'
|
||||
DEFAULT_NAME = 'Local File'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_FILE_PATH): cv.isfile,
|
||||
vol.Required(CONF_FILE_PATH): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
@@ -31,8 +31,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# check filepath given is readable
|
||||
if not os.access(file_path, os.R_OK):
|
||||
_LOGGER.error("file path is not readable")
|
||||
return False
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
config[CONF_NAME], file_path)
|
||||
|
||||
add_devices([LocalFile(config[CONF_NAME], file_path)])
|
||||
|
||||
@@ -49,8 +49,12 @@ class LocalFile(Camera):
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
with open(self._file_path, 'rb') as file:
|
||||
return file.read()
|
||||
try:
|
||||
with open(self._file_path, 'rb') as file:
|
||||
return file.read()
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
self._name, self._file_path)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -45,7 +45,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a MJPEG IP Camera."""
|
||||
yield from async_add_devices([MjpegCamera(hass, config)])
|
||||
if discovery_info:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
async_add_devices([MjpegCamera(hass, config)])
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
|
||||
@@ -14,7 +14,7 @@ import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
@@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
TIMEOUT = 5
|
||||
DEFAULT_TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
|
||||
@@ -51,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
})
|
||||
@@ -60,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||
|
||||
# Determine API to use for authentication
|
||||
@@ -74,7 +76,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
}
|
||||
query_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
@@ -103,7 +105,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
websession_init,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url
|
||||
syno_auth_url,
|
||||
timeout
|
||||
)
|
||||
|
||||
# init websession
|
||||
@@ -120,7 +123,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
camera_req = yield from websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload
|
||||
@@ -149,15 +152,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_path
|
||||
auth_path,
|
||||
timeout
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, websession, username, password, login_url):
|
||||
def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||
"""Get a session id."""
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
@@ -170,7 +174,7 @@ def get_session_id(hass, websession, username, password, login_url):
|
||||
}
|
||||
auth_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
@@ -192,7 +196,7 @@ class SynologyCamera(Camera):
|
||||
|
||||
def __init__(self, hass, websession, config, camera_id,
|
||||
camera_name, snapshot_path, streaming_path, camera_path,
|
||||
auth_path):
|
||||
auth_path, timeout):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
@@ -206,6 +210,7 @@ class SynologyCamera(Camera):
|
||||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
self._timeout = timeout
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
@@ -225,7 +230,7 @@ class SynologyCamera(Camera):
|
||||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||
response = yield from self._websession.get(
|
||||
image_url,
|
||||
params=image_payload
|
||||
|
||||
@@ -19,6 +19,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['zoneminder']
|
||||
DOMAIN = 'zoneminder'
|
||||
|
||||
# From ZoneMinder's web/includes/config.php.in
|
||||
ZM_STATE_ALARM = "2"
|
||||
|
||||
|
||||
def _get_image_url(hass, monitor, mode):
|
||||
zm_data = hass.data[DOMAIN]
|
||||
@@ -69,10 +72,43 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
|
||||
}
|
||||
cameras.append(MjpegCamera(hass, device_info))
|
||||
cameras.append(ZoneMinderCamera(hass, device_info, monitor))
|
||||
|
||||
if not cameras:
|
||||
_LOGGER.warning('No active cameras found')
|
||||
return
|
||||
|
||||
yield from async_add_devices(cameras)
|
||||
async_add_devices(cameras)
|
||||
|
||||
|
||||
class ZoneMinderCamera(MjpegCamera):
|
||||
"""Representation of a ZoneMinder Monitor Stream."""
|
||||
|
||||
def __init__(self, hass, device_info, monitor):
|
||||
"""Initialize as a subclass of MjpegCamera."""
|
||||
super().__init__(hass, device_info)
|
||||
self._monitor_id = int(monitor['Id'])
|
||||
self._is_recording = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Update the recording state periodically."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Update our recording state from the ZM API."""
|
||||
_LOGGER.debug('Updating camera state for monitor %i', self._monitor_id)
|
||||
status_response = zoneminder.get_state(
|
||||
'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
|
||||
)
|
||||
if not status_response:
|
||||
_LOGGER.warning('Could not get status for monitor %i',
|
||||
self._monitor_id)
|
||||
return
|
||||
|
||||
self._is_recording = status_response['status'] == ZM_STATE_ALARM
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return whether the monitor is in alarm mode."""
|
||||
return self._is_recording
|
||||
|
||||
@@ -224,7 +224,7 @@ def async_setup(hass, config):
|
||||
if not climate.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
climate.async_update_ha_state(True))
|
||||
if hasattr(climate, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
@@ -25,6 +25,8 @@ ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time'
|
||||
ATTR_RESUME_ALL = 'resume_all'
|
||||
|
||||
DEFAULT_RESUME_ALL = False
|
||||
TEMPERATURE_HOLD = 'temp'
|
||||
VACATION_HOLD = 'vacation'
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
|
||||
@@ -112,6 +114,8 @@ class Thermostat(ClimateDevice):
|
||||
self.thermostat_index)
|
||||
self._name = self.thermostat['name']
|
||||
self.hold_temp = hold_temp
|
||||
self.vacation = None
|
||||
self._climate_list = self.climate_list
|
||||
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
|
||||
'heat', 'off']
|
||||
self.update_without_throttle = False
|
||||
@@ -187,29 +191,30 @@ class Thermostat(ClimateDevice):
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
events = self.thermostat['events']
|
||||
if any((event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) <= 1)
|
||||
or event['type'] == 'autoAway'
|
||||
for event in events):
|
||||
# away hold is auto away or a temporary hold from away climate
|
||||
hold = 'away'
|
||||
elif any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events):
|
||||
# a permanent away is not considered a hold, but away_mode
|
||||
hold = None
|
||||
elif any(event['holdClimateRef'] == 'home' or
|
||||
event['type'] == 'autoHome'
|
||||
for event in events):
|
||||
# home mode is auto home or any home hold
|
||||
hold = 'home'
|
||||
elif any(event['type'] == 'hold' and event['running']
|
||||
for event in events):
|
||||
hold = 'temp'
|
||||
# temperature hold is any other hold not based on climate
|
||||
else:
|
||||
hold = None
|
||||
return hold
|
||||
for event in events:
|
||||
if event['running']:
|
||||
if event['type'] == 'hold':
|
||||
if event['holdClimateRef'] == 'away':
|
||||
if int(event['endDate'][0:4]) - \
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# a temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
else:
|
||||
# a premanent hold from away climate is away_mode
|
||||
return None
|
||||
elif event['holdClimateRef'] != "":
|
||||
# any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
else:
|
||||
# any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
elif event['type'].startswith('auto'):
|
||||
# all auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
elif event['type'] == 'vacation':
|
||||
self.vacation = event['name']
|
||||
return VACATION_HOLD
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
@@ -232,8 +237,11 @@ class Thermostat(ClimateDevice):
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
"""Return current mode ie. home, away, sleep."""
|
||||
return self.thermostat['program']['currentClimateRef']
|
||||
"""Return current mode, as the user-visible name."""
|
||||
cur = self.thermostat['program']['currentClimateRef']
|
||||
climates = self.thermostat['program']['climates']
|
||||
current = list(filter(lambda x: x['climateRef'] == cur, climates))
|
||||
return current[0]['name']
|
||||
|
||||
@property
|
||||
def fan_min_on_time(self):
|
||||
@@ -261,52 +269,44 @@ class Thermostat(ClimateDevice):
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"operation": operation,
|
||||
"climate_list": self.climate_list,
|
||||
"fan_min_on_time": self.fan_min_on_time
|
||||
}
|
||||
|
||||
def is_vacation_on(self):
|
||||
"""Return true if vacation mode is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['type'] == 'vacation' and event['running']
|
||||
for event in events)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events)
|
||||
return self.current_hold_mode == 'away'
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", 'indefinite')
|
||||
self.update_without_throttle = True
|
||||
self.set_hold_mode('away')
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
self.set_hold_mode(None)
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Set hold mode (away, home, temp)."""
|
||||
"""Set hold mode (away, home, temp, sleep, etc.)."""
|
||||
hold = self.current_hold_mode
|
||||
|
||||
if hold == hold_mode:
|
||||
# no change, so no action required
|
||||
return
|
||||
elif hold_mode == 'away':
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", self.hold_preference())
|
||||
elif hold_mode == 'home':
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"home", self.hold_preference())
|
||||
elif hold_mode == 'temp':
|
||||
self.set_temp_hold(int(self.current_temperature))
|
||||
elif hold_mode == 'None' or hold_mode is None:
|
||||
if hold == VACATION_HOLD:
|
||||
self.data.ecobee.delete_vacation(self.thermostat_index,
|
||||
self.vacation)
|
||||
else:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
else:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
if hold_mode == TEMPERATURE_HOLD:
|
||||
self.set_temp_hold(int(self.current_temperature))
|
||||
else:
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
hold_mode,
|
||||
self.hold_preference())
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_auto_temp_hold(self, heat_temp, cool_temp):
|
||||
"""Set temperature hold in auto mode."""
|
||||
@@ -382,3 +382,9 @@ class Thermostat(ClimateDevice):
|
||||
# as an indefinite away hold is interpreted as away_mode
|
||||
else:
|
||||
return 'nextTransition'
|
||||
|
||||
@property
|
||||
def climate_list(self):
|
||||
"""Return the list of climates currently available."""
|
||||
climates = self.thermostat['program']['climates']
|
||||
return list(map((lambda x: x['name']), climates))
|
||||
|
||||
@@ -16,7 +16,8 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_time_interval)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,6 +36,7 @@ CONF_TARGET_TEMP = 'target_temp'
|
||||
CONF_AC_MODE = 'ac_mode'
|
||||
CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_TOLERANCE = 'tolerance'
|
||||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -47,6 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_KEEP_ALIVE): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
})
|
||||
|
||||
|
||||
@@ -62,10 +66,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
ac_mode = config.get(CONF_AC_MODE)
|
||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
tolerance = config.get(CONF_TOLERANCE)
|
||||
keep_alive = config.get(CONF_KEEP_ALIVE)
|
||||
|
||||
yield from async_add_devices([GenericThermostat(
|
||||
async_add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance)])
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance, keep_alive)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
@@ -73,7 +78,7 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
tolerance):
|
||||
tolerance, keep_alive):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@@ -81,6 +86,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self.ac_mode = ac_mode
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
self._keep_alive = keep_alive
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@@ -94,6 +100,10 @@ class GenericThermostat(ClimateDevice):
|
||||
async_track_state_change(
|
||||
hass, heater_entity_id, self._async_switch_changed)
|
||||
|
||||
if self._keep_alive:
|
||||
async_track_time_interval(
|
||||
hass, self._async_keep_alive, self._keep_alive)
|
||||
|
||||
sensor_state = hass.states.get(sensor_entity_id)
|
||||
if sensor_state:
|
||||
self._async_update_temp(sensor_state)
|
||||
@@ -180,6 +190,14 @@ class GenericThermostat(ClimateDevice):
|
||||
return
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@callback
|
||||
def _async_keep_alive(self, time):
|
||||
"""Called at constant intervals for keep-alive purposes."""
|
||||
if self.current_operation in [STATE_COOL, STATE_HEAT]:
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
|
||||
@callback
|
||||
def _async_update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
|
||||
@@ -6,10 +6,15 @@ https://home-assistant.io/components/climate.honeywell/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA,
|
||||
ATTR_FAN_MODE, ATTR_FAN_LIST,
|
||||
ATTR_OPERATION_MODE,
|
||||
ATTR_OPERATION_LIST)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE)
|
||||
@@ -21,27 +26,35 @@ REQUIREMENTS = ['evohomeclient==0.2.5',
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
ATTR_FANMODE = 'fanmode'
|
||||
ATTR_SYSTEM_MODE = 'system_mode'
|
||||
ATTR_CURRENT_OPERATION = 'equipment_output_status'
|
||||
|
||||
CONF_AWAY_TEMPERATURE = 'away_temperature'
|
||||
CONF_COOL_AWAY_TEMPERATURE = 'away_cool_temperature'
|
||||
CONF_HEAT_AWAY_TEMPERATURE = 'away_heat_temperature'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE = 16
|
||||
DEFAULT_COOL_AWAY_TEMPERATURE = 30
|
||||
DEFAULT_HEAT_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_AWAY_TEMPERATURE,
|
||||
default=DEFAULT_AWAY_TEMPERATURE): vol.Coerce(float),
|
||||
vol.Optional(CONF_COOL_AWAY_TEMPERATURE,
|
||||
default=DEFAULT_COOL_AWAY_TEMPERATURE): vol.Coerce(float),
|
||||
vol.Optional(CONF_HEAT_AWAY_TEMPERATURE,
|
||||
default=DEFAULT_HEAT_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."""
|
||||
"""Setup the Honeywell thermostat."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
region = config.get(CONF_REGION)
|
||||
@@ -88,8 +101,11 @@ def _setup_us(username, password, config, add_devices):
|
||||
|
||||
dev_id = config.get('thermostat')
|
||||
loc_id = config.get('location')
|
||||
cool_away_temp = config.get(CONF_COOL_AWAY_TEMPERATURE)
|
||||
heat_away_temp = config.get(CONF_HEAT_AWAY_TEMPERATURE)
|
||||
|
||||
add_devices([HoneywellUSThermostat(client, device)
|
||||
add_devices([HoneywellUSThermostat(client, device, cool_away_temp,
|
||||
heat_away_temp, username, password)
|
||||
for location in client.locations_by_id.values()
|
||||
for device in location.devices_by_id.values()
|
||||
if ((not loc_id or location.locationid == loc_id) and
|
||||
@@ -160,7 +176,7 @@ class RoundThermostat(ClimateDevice):
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on.
|
||||
|
||||
Evohome does have a proprietary away mode, but it doesn't really work
|
||||
Honeywell does have a proprietary away mode, but it doesn't really work
|
||||
the way it should. For example: If you set a temperature manually
|
||||
it doesn't get overwritten when away mode is switched on.
|
||||
"""
|
||||
@@ -199,10 +215,16 @@ class RoundThermostat(ClimateDevice):
|
||||
class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Representation of a Honeywell US Thermostat."""
|
||||
|
||||
def __init__(self, client, device):
|
||||
def __init__(self, client, device, cool_away_temp,
|
||||
heat_away_temp, username, password):
|
||||
"""Initialize the thermostat."""
|
||||
self._client = client
|
||||
self._device = device
|
||||
self._cool_away_temp = cool_away_temp
|
||||
self._heat_away_temp = heat_away_temp
|
||||
self._away = False
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
@property
|
||||
def is_fan_on(self):
|
||||
@@ -236,7 +258,10 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return getattr(self._device, ATTR_SYSTEM_MODE, None)
|
||||
oper = getattr(self._device, ATTR_CURRENT_OPERATION, None)
|
||||
if oper == "off":
|
||||
oper = "idle"
|
||||
return oper
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set target temperature."""
|
||||
@@ -245,29 +270,84 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
return
|
||||
import somecomfort
|
||||
try:
|
||||
if self._device.system_mode == 'cool':
|
||||
self._device.setpoint_cool = temperature
|
||||
else:
|
||||
self._device.setpoint_heat = temperature
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
# Set hold if this is not the case
|
||||
if getattr(self._device, "hold_{}".format(mode)) is False:
|
||||
# Get next period key
|
||||
next_period_key = '{}NextPeriod'.format(mode.capitalize())
|
||||
# Get next period raw value
|
||||
next_period = self._device.raw_ui_data.get(next_period_key)
|
||||
# Get next period time
|
||||
hour, minute = divmod(next_period * 15, 60)
|
||||
# Set hold time
|
||||
setattr(self._device,
|
||||
"hold_{}".format(mode),
|
||||
datetime.time(hour, minute))
|
||||
# Set temperature
|
||||
setattr(self._device,
|
||||
"setpoint_{}".format(mode),
|
||||
temperature)
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Temperature %.1f out of range', temperature)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
import somecomfort
|
||||
data = {
|
||||
ATTR_FAN: (self.is_fan_on and 'running' or 'idle'),
|
||||
ATTR_FANMODE: self._device.fan_mode,
|
||||
ATTR_SYSTEM_MODE: self._device.system_mode,
|
||||
ATTR_FAN_MODE: self._device.fan_mode,
|
||||
ATTR_OPERATION_MODE: self._device.system_mode,
|
||||
}
|
||||
data[ATTR_FAN_LIST] = somecomfort.FAN_MODES
|
||||
data[ATTR_OPERATION_LIST] = somecomfort.SYSTEM_MODES
|
||||
return data
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
pass
|
||||
"""Turn away on.
|
||||
|
||||
Somecomfort does have a proprietary away mode, but it doesn't really
|
||||
work the way it should. For example: If you set a temperature manually
|
||||
it doesn't get overwritten when away mode is switched on.
|
||||
"""
|
||||
self._away = True
|
||||
import somecomfort
|
||||
try:
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Can not get system mode')
|
||||
return
|
||||
try:
|
||||
|
||||
# Set permanent hold
|
||||
setattr(self._device,
|
||||
"hold_{}".format(mode),
|
||||
True)
|
||||
# Set temperature
|
||||
setattr(self._device,
|
||||
"setpoint_{}".format(mode),
|
||||
getattr(self, "_{}_away_temp".format(mode)))
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Temperature %.1f out of range',
|
||||
getattr(self, "_{}_away_temp".format(mode)))
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
pass
|
||||
self._away = False
|
||||
import somecomfort
|
||||
try:
|
||||
# Disabling all hold modes
|
||||
self._device.hold_cool = False
|
||||
self._device.hold_heat = False
|
||||
except somecomfort.SomeComfortError:
|
||||
_LOGGER.error('Can not stop hold mode')
|
||||
|
||||
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
|
||||
"""Set the system mode (Cool, Heat, etc)."""
|
||||
@@ -276,4 +356,49 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
self._device.refresh()
|
||||
import somecomfort
|
||||
retries = 3
|
||||
while retries > 0:
|
||||
try:
|
||||
self._device.refresh()
|
||||
break
|
||||
except (somecomfort.client.APIRateLimited, OSError,
|
||||
requests.exceptions.ReadTimeout) as exp:
|
||||
retries -= 1
|
||||
if retries == 0:
|
||||
raise exp
|
||||
if not self._retry():
|
||||
raise exp
|
||||
_LOGGER.error("SomeComfort update failed, Retrying "
|
||||
"- Error: %s", exp)
|
||||
|
||||
def _retry(self):
|
||||
"""Recreate a new somecomfort client.
|
||||
|
||||
When we got an error, the best way to be sure that the next query
|
||||
will succeed, is to recreate a new somecomfort client.
|
||||
"""
|
||||
import somecomfort
|
||||
try:
|
||||
self._client = somecomfort.SomeComfort(self._username,
|
||||
self._password)
|
||||
except somecomfort.AuthError:
|
||||
_LOGGER.error('Failed to login to honeywell account %s',
|
||||
self._username)
|
||||
return False
|
||||
except somecomfort.SomeComfortError as ex:
|
||||
_LOGGER.error('Failed to initialize honeywell client: %s',
|
||||
str(ex))
|
||||
return False
|
||||
|
||||
devices = [device
|
||||
for location in self._client.locations_by_id.values()
|
||||
for device in location.devices_by_id.values()
|
||||
if device.name == self._device.name]
|
||||
|
||||
if len(devices) != 1:
|
||||
_LOGGER.error('Failed to find device %s', self._device.name)
|
||||
return False
|
||||
|
||||
self._device = devices[0]
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Support for MAX! Thermostats via MAX! Cube.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/maxcube/
|
||||
"""
|
||||
|
||||
import socket
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_MANUAL = "manual"
|
||||
STATE_BOOST = "boost"
|
||||
STATE_VACATION = "vacation"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add thermostats to HASS."""
|
||||
cube = hass.data[MAXCUBE_HANDLE].cube
|
||||
|
||||
# List of devices
|
||||
devices = []
|
||||
|
||||
for device in cube.devices:
|
||||
# Create device name by concatenating room name + device name
|
||||
name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name)
|
||||
|
||||
# Only add thermostats and wallthermostats
|
||||
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
|
||||
# Add device to HASS
|
||||
devices.append(MaxCubeClimate(hass, name, device.rf_address))
|
||||
|
||||
# Add all devices at once
|
||||
if len(devices) > 0:
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class MaxCubeClimate(ClimateDevice):
|
||||
"""MAX! Cube ClimateDevice."""
|
||||
|
||||
def __init__(self, hass, name, rf_address):
|
||||
"""Initialize MAX! Cube ClimateDevice."""
|
||||
self._name = name
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
|
||||
STATE_VACATION]
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = hass.data[MAXCUBE_HANDLE]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is required."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the ClimateDevice."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return minimum temperature
|
||||
return self.map_temperature_max_hass(device.min_temperature)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return maximum temperature
|
||||
return self.map_temperature_max_hass(device.max_temperature)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return current temperature
|
||||
return self.map_temperature_max_hass(device.actual_temperature)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation (auto, manual, boost, vacation)."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Mode Mapping
|
||||
return self.map_mode_max_hass(device.mode)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
# Get the device we want (does not do any IO, just reads from memory)
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Map and return target temperature
|
||||
return self.map_temperature_max_hass(device.target_temperature)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
# Fail is target temperature has not been supplied as argument
|
||||
if kwargs.get(ATTR_TEMPERATURE) is None:
|
||||
return False
|
||||
|
||||
# Determine the new target temperature
|
||||
target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
# Write the target temperature to the MAX! Cube.
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
cube = self._cubehandle.cube
|
||||
|
||||
with self._cubehandle.mutex:
|
||||
try:
|
||||
cube.set_target_temperature(device, target_temperature)
|
||||
except (socket.timeout, socket.error):
|
||||
_LOGGER.error("Setting target temperature failed")
|
||||
return False
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new operation mode."""
|
||||
# Get the device we want to update
|
||||
device = self._cubehandle.cube.device_by_rf(self._rf_address)
|
||||
|
||||
# Mode Mapping
|
||||
mode = self.map_mode_hass_max(operation_mode)
|
||||
|
||||
# Write new mode to thermostat
|
||||
if mode is None:
|
||||
return False
|
||||
|
||||
with self._cubehandle.mutex:
|
||||
try:
|
||||
self._cubehandle.cube.set_mode(device, mode)
|
||||
except (socket.timeout, socket.error):
|
||||
_LOGGER.error("Setting operation mode failed")
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Get latest data from MAX! Cube."""
|
||||
# Update the CubeHandle
|
||||
self._cubehandle.update()
|
||||
|
||||
@staticmethod
|
||||
def map_temperature_max_hass(temperature):
|
||||
"""Map Temperature from MAX! to HASS."""
|
||||
if temperature is None:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
return temperature
|
||||
|
||||
@staticmethod
|
||||
def map_mode_hass_max(operation_mode):
|
||||
"""Map HASS Operation Modes to MAX! Operation Modes."""
|
||||
from maxcube.device import \
|
||||
MAX_DEVICE_MODE_AUTOMATIC, \
|
||||
MAX_DEVICE_MODE_MANUAL, \
|
||||
MAX_DEVICE_MODE_VACATION, \
|
||||
MAX_DEVICE_MODE_BOOST
|
||||
|
||||
if operation_mode == STATE_AUTO:
|
||||
mode = MAX_DEVICE_MODE_AUTOMATIC
|
||||
elif operation_mode == STATE_MANUAL:
|
||||
mode = MAX_DEVICE_MODE_MANUAL
|
||||
elif operation_mode == STATE_VACATION:
|
||||
mode = MAX_DEVICE_MODE_VACATION
|
||||
elif operation_mode == STATE_BOOST:
|
||||
mode = MAX_DEVICE_MODE_BOOST
|
||||
else:
|
||||
mode = None
|
||||
|
||||
return mode
|
||||
|
||||
@staticmethod
|
||||
def map_mode_max_hass(mode):
|
||||
"""Map MAX! Operation Modes to HASS Operation Modes."""
|
||||
from maxcube.device import \
|
||||
MAX_DEVICE_MODE_AUTOMATIC, \
|
||||
MAX_DEVICE_MODE_MANUAL, \
|
||||
MAX_DEVICE_MODE_VACATION, \
|
||||
MAX_DEVICE_MODE_BOOST
|
||||
|
||||
if mode == MAX_DEVICE_MODE_AUTOMATIC:
|
||||
operation_mode = STATE_AUTO
|
||||
elif mode == MAX_DEVICE_MODE_MANUAL:
|
||||
operation_mode = STATE_MANUAL
|
||||
elif mode == MAX_DEVICE_MODE_VACATION:
|
||||
operation_mode = STATE_VACATION
|
||||
elif mode == MAX_DEVICE_MODE_BOOST:
|
||||
operation_mode = STATE_BOOST
|
||||
else:
|
||||
operation_mode = None
|
||||
|
||||
return operation_mode
|
||||
@@ -0,0 +1,296 @@
|
||||
"""tado component to create a climate device for each zone."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE)
|
||||
from homeassistant.components.tado import (
|
||||
DATA_TADO)
|
||||
|
||||
CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode
|
||||
CONST_MODE_OFF = "OFF" # Switch off heating in a zone
|
||||
|
||||
# When we change the temperature setting, we need an overlay mode
|
||||
# wait until tado changes the mode automatic
|
||||
CONST_OVERLAY_TADO_MODE = "TADO_MODE"
|
||||
# the user has change the temperature or mode manually
|
||||
CONST_OVERLAY_MANUAL = "MANUAL"
|
||||
# the temperature will be reset after a timespan
|
||||
CONST_OVERLAY_TIMER = "TIMER"
|
||||
|
||||
OPERATION_LIST = {
|
||||
CONST_OVERLAY_MANUAL: "Manual",
|
||||
CONST_OVERLAY_TIMER: "Timer",
|
||||
CONST_OVERLAY_TADO_MODE: "Tado mode",
|
||||
CONST_MODE_SMART_SCHEDULE: "Smart schedule",
|
||||
CONST_MODE_OFF: "Off"}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the climate platform."""
|
||||
# get the PyTado object from the hub component
|
||||
tado = hass.data[DATA_TADO]
|
||||
|
||||
try:
|
||||
zones = tado.get_zones()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to get zone info from mytado")
|
||||
return False
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
climate_devices.append(create_climate_device(tado, hass,
|
||||
zone,
|
||||
zone['name'],
|
||||
zone['id']))
|
||||
|
||||
if len(climate_devices) > 0:
|
||||
add_devices(climate_devices, True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
"""Create a climate device."""
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
min_temp = float(capabilities["temperatures"]["celsius"]["min"])
|
||||
max_temp = float(capabilities["temperatures"]["celsius"]["max"])
|
||||
ac_mode = capabilities["type"] != "HEATING"
|
||||
|
||||
data_id = 'zone {} {}'.format(name, zone_id)
|
||||
device = TadoClimate(tado,
|
||||
name, zone_id, data_id,
|
||||
hass.config.units.temperature(min_temp, unit),
|
||||
hass.config.units.temperature(max_temp, unit),
|
||||
ac_mode)
|
||||
|
||||
tado.add_sensor(data_id, {
|
||||
"id": zone_id,
|
||||
"zone": zone,
|
||||
"name": name,
|
||||
"climate": device
|
||||
})
|
||||
|
||||
return device
|
||||
|
||||
|
||||
class TadoClimate(ClimateDevice):
|
||||
"""Representation of a tado climate device."""
|
||||
|
||||
def __init__(self, store, zone_name, zone_id, data_id,
|
||||
min_temp, max_temp, ac_mode,
|
||||
tolerance=0.3):
|
||||
"""Initialization of TadoClimate device."""
|
||||
self._store = store
|
||||
self._data_id = data_id
|
||||
|
||||
self.zone_name = zone_name
|
||||
self.zone_id = zone_id
|
||||
|
||||
self.ac_mode = ac_mode
|
||||
|
||||
self._active = False
|
||||
self._device_is_active = False
|
||||
|
||||
self._unit = TEMP_CELSIUS
|
||||
self._cur_temp = None
|
||||
self._cur_humidity = None
|
||||
self._is_away = False
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = None
|
||||
self._tolerance = tolerance
|
||||
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self.zone_name
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._cur_humidity
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the sensor temperature."""
|
||||
return self._cur_temp
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current readable operation mode."""
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes (readable)."""
|
||||
return list(OPERATION_LIST.values())
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""The unit of measurement used by the platform."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._is_away
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
self._current_operation = CONST_OVERLAY_TADO_MODE
|
||||
self._overlay_mode = None
|
||||
self._target_temp = temperature
|
||||
self._control_heating()
|
||||
|
||||
def set_operation_mode(self, readable_operation_mode):
|
||||
"""Set new operation mode."""
|
||||
operation_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
for mode, readable in OPERATION_LIST.items():
|
||||
if readable == readable_operation_mode:
|
||||
operation_mode = mode
|
||||
break
|
||||
|
||||
self._current_operation = operation_mode
|
||||
self._overlay_mode = None
|
||||
self._control_heating()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
self._store.update()
|
||||
|
||||
data = self._store.get_data(self._data_id)
|
||||
|
||||
if data is None:
|
||||
_LOGGER.debug('Recieved no data for zone %s',
|
||||
self.zone_name)
|
||||
return
|
||||
|
||||
if 'sensorDataPoints' in data:
|
||||
sensor_data = data['sensorDataPoints']
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
humidity = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
setting = 0
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if 'temperature' in data['setting'] and \
|
||||
data['setting']['temperature'] is not None:
|
||||
setting = float(
|
||||
data['setting']['temperature']['celsius'])
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
self._cur_temp = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
self._cur_humidity = humidity
|
||||
|
||||
if 'tadoMode' in data:
|
||||
mode = data['tadoMode']
|
||||
self._is_away = mode == "AWAY"
|
||||
|
||||
if 'setting' in data:
|
||||
power = data['setting']['power']
|
||||
if power == "OFF":
|
||||
self._current_operation = CONST_MODE_OFF
|
||||
self._device_is_active = False
|
||||
else:
|
||||
self._device_is_active = True
|
||||
|
||||
if 'overlay' in data and data['overlay'] is not None:
|
||||
overlay = True
|
||||
termination = data['overlay']['termination']['type']
|
||||
else:
|
||||
overlay = False
|
||||
termination = ""
|
||||
|
||||
# if you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
|
||||
if overlay and self._device_is_active:
|
||||
# there is an overlay the device is on
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
else:
|
||||
# there is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
def _control_heating(self):
|
||||
"""Send new target temperature to mytado."""
|
||||
if not self._active and None not in (self._cur_temp,
|
||||
self._target_temp):
|
||||
self._active = True
|
||||
_LOGGER.info('Obtained current and target temperature. '
|
||||
'tado thermostat active.')
|
||||
|
||||
if not self._active or self._current_operation == self._overlay_mode:
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
||||
_LOGGER.info('Switching mytado.com to SCHEDULE (default) '
|
||||
'for zone %s', self.zone_name)
|
||||
self._store.reset_zone_overlay(self.zone_id)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_OFF:
|
||||
_LOGGER.info('Switching mytado.com to OFF for zone %s',
|
||||
self.zone_name)
|
||||
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
_LOGGER.info('Switching mytado.com to %s mode for zone %s',
|
||||
self._current_operation, self.zone_name)
|
||||
self._store.set_zone_overlay(self.zone_id,
|
||||
self._current_operation,
|
||||
self._target_temp)
|
||||
|
||||
self._overlay_mode = self._current_operation
|
||||
@@ -7,14 +7,14 @@ https://home-assistant.io/components/switch.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE)
|
||||
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@@ -37,6 +37,7 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
@@ -84,11 +85,11 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
return self.vera_device.fan_cycle()
|
||||
|
||||
@property
|
||||
def current_power_mwh(self):
|
||||
"""Current power usage in mWh."""
|
||||
def current_power_w(self):
|
||||
"""Current power usage in W."""
|
||||
power = self.vera_device.power
|
||||
if power:
|
||||
return convert(power, float, 0.0) * 1000
|
||||
return convert(power, float, 0.0)
|
||||
|
||||
def update(self):
|
||||
"""Called by the vera device callback to update state."""
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
|
||||
@@ -32,29 +32,18 @@ DEVICE_MAPPINGS = {
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Z-Wave Climate devices."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return
|
||||
def get_device(hass, values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
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)
|
||||
add_devices([ZWaveClimate(value, temp_unit)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return ZWaveClimate(values, temp_unit)
|
||||
|
||||
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
def __init__(self, value, temp_unit):
|
||||
def __init__(self, values, temp_unit):
|
||||
"""Initialize the Z-Wave climate device."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._index = value.index
|
||||
self._node = value.node
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_operation = None
|
||||
@@ -69,10 +58,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16))
|
||||
if (self.node.manufacturer_id.strip() and
|
||||
self.node.product_id.strip()):
|
||||
specific_sensor_key = (
|
||||
int(self.node.manufacturer_id, 16),
|
||||
int(self.node.product_id, 16))
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
|
||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||
@@ -83,86 +73,58 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Operation Mode
|
||||
self._current_operation = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, member='data')
|
||||
operation_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
member='data_items')
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
if self.values.mode:
|
||||
self._current_operation = self.values.mode.data
|
||||
operation_list = self.values.mode.data_items
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s", self._current_operation)
|
||||
|
||||
# Current Temp
|
||||
self._current_temperature = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='data')
|
||||
device_unit = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='units')
|
||||
if device_unit is not None:
|
||||
self._unit = device_unit
|
||||
if self.values.temperature:
|
||||
self._current_temperature = self.values.temperature.data
|
||||
device_unit = self.values.temperature.units
|
||||
if device_unit is not None:
|
||||
self._unit = device_unit
|
||||
|
||||
# Fan Mode
|
||||
self._current_fan_mode = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data')
|
||||
fan_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data_items')
|
||||
if fan_list:
|
||||
self._fan_list = list(fan_list)
|
||||
if self.values.fan_mode:
|
||||
self._current_fan_mode = self.values.fan_mode.data
|
||||
fan_list = self.values.fan_mode.data_items
|
||||
if fan_list:
|
||||
self._fan_list = list(fan_list)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
_LOGGER.debug("self._current_fan_mode=%s",
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
self._current_swing_mode = (
|
||||
self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data'))
|
||||
swing_list = self.get_value(class_id=zwave.const
|
||||
.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data_items')
|
||||
if swing_list:
|
||||
self._swing_list = list(swing_list)
|
||||
if self.values.zxt_120_swing_mode:
|
||||
self._current_swing_mode = self.values.zxt_120_swing_mode.data
|
||||
swing_list = self.values.zxt_120_swing_mode.data_items
|
||||
if swing_list:
|
||||
self._swing_list = list(swing_list)
|
||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
temps = []
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
temps.append((round(float(value.data)), 1))
|
||||
if value.index == self._index:
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
break
|
||||
else:
|
||||
self._target_temperature = round((float(value.data)), 1)
|
||||
if self.values.primary.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
else:
|
||||
self._target_temperature = round(
|
||||
(float(self.values.primary.data)), 1)
|
||||
|
||||
# Operating state
|
||||
self._operating_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
|
||||
member='data')
|
||||
if self.values.operating_state:
|
||||
self._operating_state = self.values.operating_state.data
|
||||
|
||||
# Fan operating state
|
||||
self._fan_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE,
|
||||
member='data')
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling on Z-Wave."""
|
||||
return False
|
||||
if self.values.fan_state:
|
||||
self._fan_state = self.values.fan_state.data
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
@@ -221,36 +183,31 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
else:
|
||||
return
|
||||
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT,
|
||||
index=self._index, data=temperature)
|
||||
self.update_ha_state()
|
||||
self.values.primary.data = temperature
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
index=0, data=bytes(fan, 'utf-8'))
|
||||
if self.values.fan_mode:
|
||||
self.values.fan_mode.data = bytes(fan, 'utf-8')
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
index=0, data=bytes(operation_mode, 'utf-8'))
|
||||
if self.values.mode:
|
||||
self.values.mode.data = bytes(operation_mode, 'utf-8')
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
if self._zxt_120 == 1:
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33, data=bytes(swing_mode, 'utf-8'))
|
||||
if self.values.zxt_120_swing_mode:
|
||||
self.values.zxt_120_swing_mode.data = bytes(
|
||||
swing_mode, 'utf-8')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
data = super().device_state_attributes
|
||||
if self._operating_state:
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state,
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state
|
||||
if self._fan_state:
|
||||
data[ATTR_FAN_STATE] = self._fan_state
|
||||
return data
|
||||
|
||||
@@ -6,7 +6,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.bootstrap import (
|
||||
from homeassistant.setup import (
|
||||
async_prepare_setup_platform, ATTR_COMPONENT)
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
@@ -129,5 +129,8 @@ def _read(path):
|
||||
|
||||
def _write(path, data):
|
||||
"""Write YAML helper."""
|
||||
# Do it before opening file. If dump causes error it will now not
|
||||
# truncate the file.
|
||||
data = dump(data)
|
||||
with open(path, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(dump(data))
|
||||
outfile.write(data)
|
||||
|
||||
@@ -166,7 +166,7 @@ def async_setup(hass, config):
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
cover.async_update_ha_state(True))
|
||||
if hasattr(cover, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
@@ -54,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
yield from async_add_devices([MqttCover(
|
||||
async_add_devices([MqttCover(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
||||
@@ -14,8 +14,8 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.6.zip'
|
||||
'#pymyq==0.0.6']
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
|
||||
'#pymyq==0.0.8']
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): cv.string,
|
||||
|
||||
@@ -6,9 +6,9 @@ https://home-assistant.io/components/cover.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@@ -28,6 +28,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.cover import (
|
||||
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.components.zwave import workaround
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
|
||||
@@ -19,42 +20,31 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave covers."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
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.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and value.index == 0):
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.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)])
|
||||
else:
|
||||
return
|
||||
def get_device(values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
if (values.primary.command_class ==
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and values.primary.index == 0):
|
||||
return ZwaveRollershutter(values)
|
||||
elif (values.primary.command_class in [
|
||||
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
||||
return ZwaveGarageDoor(values)
|
||||
return None
|
||||
|
||||
|
||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave roller shutter."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, values):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._node = value.node
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
self._current_position = None
|
||||
|
||||
self._workaround = workaround.get_device_mapping(value)
|
||||
self._workaround = workaround.get_device_mapping(values.primary)
|
||||
if self._workaround:
|
||||
_LOGGER.debug("Using workaround %s", self._workaround)
|
||||
self.update_properties()
|
||||
@@ -62,17 +52,17 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Position value
|
||||
self._current_position = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Level'], member='data')
|
||||
self._open_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Open', 'Up'], member='value_id')
|
||||
self._close_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Close', 'Down'], member='value_id')
|
||||
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
|
||||
self._open_id, self._close_id = self._close_id, self._open_id
|
||||
self._current_position = self.values.primary.data
|
||||
|
||||
if self.values.open and self.values.close and \
|
||||
self._open_id is None and self._close_id is None:
|
||||
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
|
||||
self._open_id = self.values.close.value_id
|
||||
self._close_id = self.values.open.value_id
|
||||
self._workaround = None
|
||||
else:
|
||||
self._open_id = self.values.open.value_id
|
||||
self._close_id = self.values.close.value_id
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
@@ -107,7 +97,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
self._node.set_dimmer(self._value.value_id, position)
|
||||
self.node.set_dimmer(self.values.primary.value_id, position)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
@@ -117,14 +107,14 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave garage door device."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, values):
|
||||
"""Initialize the zwave garage door."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
@@ -133,11 +123,11 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the garage door."""
|
||||
self._value.data = False
|
||||
self.values.primary.data = False
|
||||
|
||||
def open_cover(self):
|
||||
"""Open the garage door."""
|
||||
self._value.data = True
|
||||
self.values.primary.data = True
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -4,6 +4,7 @@ Sets up a demo environment that mimics interaction with devices.
|
||||
For more details about this component, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
@@ -34,7 +35,8 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup a demo environment."""
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
@@ -44,7 +46,7 @@ def setup(hass, config):
|
||||
config.setdefault(DOMAIN, {})
|
||||
|
||||
if config[DOMAIN].get('hide_demo_state') != 1:
|
||||
hass.states.set('a.Demo_Mode', 'Enabled')
|
||||
hass.states.async_set('a.Demo_Mode', 'Enabled')
|
||||
|
||||
# Setup sun
|
||||
if not hass.config.latitude:
|
||||
@@ -53,50 +55,71 @@ def setup(hass, config):
|
||||
if not hass.config.longitude:
|
||||
hass.config.longitude = 117.22743
|
||||
|
||||
bootstrap.setup_component(hass, 'sun')
|
||||
tasks = [
|
||||
bootstrap.async_setup_component(hass, 'sun')
|
||||
]
|
||||
|
||||
# Setup demo platforms
|
||||
demo_config = config.copy()
|
||||
for component in COMPONENTS_WITH_DEMO_PLATFORM:
|
||||
demo_config[component] = {CONF_PLATFORM: 'demo'}
|
||||
bootstrap.setup_component(hass, component, demo_config)
|
||||
tasks.append(
|
||||
bootstrap.async_setup_component(hass, component, demo_config))
|
||||
|
||||
# Set up input select
|
||||
tasks.append(bootstrap.async_setup_component(
|
||||
hass, 'input_select',
|
||||
{'input_select':
|
||||
{'living_room_preset': {'options': ['Visitors',
|
||||
'Visitors with kids',
|
||||
'Home Alone']},
|
||||
'who_cooks': {'icon': 'mdi:panda',
|
||||
'initial': 'Anne Therese',
|
||||
'name': 'Cook today',
|
||||
'options': ['Paulus', 'Anne Therese']}}}))
|
||||
# Set up input boolean
|
||||
tasks.append(bootstrap.async_setup_component(
|
||||
hass, 'input_boolean',
|
||||
{'input_boolean': {'notify': {
|
||||
'icon': 'mdi:car',
|
||||
'initial': False,
|
||||
'name': 'Notify Anne Therese is home'}}}))
|
||||
|
||||
# Set up input boolean
|
||||
tasks.append(bootstrap.async_setup_component(
|
||||
hass, 'input_slider',
|
||||
{'input_slider': {
|
||||
'noise_allowance': {'icon': 'mdi:bell-ring',
|
||||
'min': 0,
|
||||
'max': 10,
|
||||
'name': 'Allowed Noise',
|
||||
'unit_of_measurement': 'dB'}}}))
|
||||
|
||||
# Set up weblink
|
||||
tasks.append(bootstrap.async_setup_component(
|
||||
hass, 'weblink',
|
||||
{'weblink': {'entities': [{'name': 'Router',
|
||||
'url': 'http://192.168.1.1'}]}}))
|
||||
|
||||
results = yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
|
||||
if any(not result for result in results):
|
||||
return False
|
||||
|
||||
# Setup example persistent notification
|
||||
persistent_notification.create(
|
||||
persistent_notification.async_create(
|
||||
hass, 'This is an example of a persistent notification.',
|
||||
title='Example Notification')
|
||||
|
||||
# Setup room groups
|
||||
lights = sorted(hass.states.entity_ids('light'))
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
media_players = sorted(hass.states.entity_ids('media_player'))
|
||||
lights = sorted(hass.states.async_entity_ids('light'))
|
||||
switches = sorted(hass.states.async_entity_ids('switch'))
|
||||
media_players = sorted(hass.states.async_entity_ids('media_player'))
|
||||
|
||||
group.Group.create_group(hass, 'living room', [
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'scene.romantic_lights'])
|
||||
group.Group.create_group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance'])
|
||||
group.Group.create_group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
|
||||
group.Group.create_group(hass, 'doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door'])
|
||||
group.Group.create_group(hass, 'automations', [
|
||||
'input_select.who_cooks', 'input_boolean.notify', ])
|
||||
group.Group.create_group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus'])
|
||||
group.Group.create_group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window', 'group.doors',
|
||||
'thermostat.ecobee',
|
||||
], view=True)
|
||||
tasks2 = []
|
||||
|
||||
# Setup scripts
|
||||
bootstrap.setup_component(
|
||||
tasks2.append(bootstrap.async_setup_component(
|
||||
hass, 'script',
|
||||
{'script': {
|
||||
'demo': {
|
||||
@@ -115,10 +138,10 @@ def setup(hass, config):
|
||||
'service': 'light.turn_off',
|
||||
'data': {ATTR_ENTITY_ID: lights[0]}
|
||||
}]
|
||||
}}})
|
||||
}}}))
|
||||
|
||||
# Setup scenes
|
||||
bootstrap.setup_component(
|
||||
tasks2.append(bootstrap.async_setup_component(
|
||||
hass, 'scene',
|
||||
{'scene': [
|
||||
{'name': 'Romantic lights',
|
||||
@@ -132,41 +155,37 @@ def setup(hass, config):
|
||||
switches[0]: True,
|
||||
switches[1]: False,
|
||||
}},
|
||||
]})
|
||||
]}))
|
||||
|
||||
# Set up input select
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_select',
|
||||
{'input_select':
|
||||
{'living_room_preset': {'options': ['Visitors',
|
||||
'Visitors with kids',
|
||||
'Home Alone']},
|
||||
'who_cooks': {'icon': 'mdi:panda',
|
||||
'initial': 'Anne Therese',
|
||||
'name': 'Cook today',
|
||||
'options': ['Paulus', 'Anne Therese']}}})
|
||||
# Set up input boolean
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_boolean',
|
||||
{'input_boolean': {'notify': {'icon': 'mdi:car',
|
||||
'initial': False,
|
||||
'name': 'Notify Anne Therese is home'}}})
|
||||
tasks2.append(group.Group.async_create_group(hass, 'living room', [
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'scene.romantic_lights']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'automations', [
|
||||
'input_select.who_cooks', 'input_boolean.notify', ]))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window', 'group.doors',
|
||||
'thermostat.ecobee',
|
||||
], view=True))
|
||||
|
||||
# Set up input boolean
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_slider',
|
||||
{'input_slider': {
|
||||
'noise_allowance': {'icon': 'mdi:bell-ring',
|
||||
'min': 0,
|
||||
'max': 10,
|
||||
'name': 'Allowed Noise',
|
||||
'unit_of_measurement': 'dB'}}})
|
||||
results = yield from asyncio.gather(*tasks2, loop=hass.loop)
|
||||
|
||||
if any(not result for result in results):
|
||||
return False
|
||||
|
||||
# Set up weblink
|
||||
bootstrap.setup_component(
|
||||
hass, 'weblink',
|
||||
{'weblink': {'entities': [{'name': 'Router',
|
||||
'url': 'http://192.168.1.1'}]}})
|
||||
# Setup configurator
|
||||
configurator_ids = []
|
||||
|
||||
@@ -184,14 +203,17 @@ def setup(hass, config):
|
||||
else:
|
||||
configurator.request_done(configurator_ids[0])
|
||||
|
||||
request_id = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
description=("Press the button on the bridge to register Philips Hue "
|
||||
"with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
def setup_configurator():
|
||||
"""Setup configurator."""
|
||||
request_id = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
description=("Press the button on the bridge to register Philips "
|
||||
"Hue with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
configurator_ids.append(request_id)
|
||||
|
||||
configurator_ids.append(request_id)
|
||||
hass.async_add_job(setup_configurator)
|
||||
|
||||
return True
|
||||
|
||||
@@ -14,12 +14,11 @@ import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import (
|
||||
async_prepare_setup_platform, async_log_exception)
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
@@ -133,18 +132,6 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
||||
|
||||
# added_to_hass
|
||||
add_tasks = [device.async_added_to_hass() for device in devices
|
||||
if device.track]
|
||||
if add_tasks:
|
||||
yield from asyncio.wait(add_tasks, loop=hass.loop)
|
||||
|
||||
# update tracked devices
|
||||
update_tasks = [device.async_update_ha_state() for device in devices
|
||||
if device.track]
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(p_type, p_config, disc_info=None):
|
||||
"""Setup a device tracker platform."""
|
||||
@@ -227,6 +214,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
|
||||
|
||||
# restore
|
||||
yield from tracker.async_setup_tracked_device()
|
||||
return True
|
||||
|
||||
|
||||
@@ -357,6 +346,27 @@ class DeviceTracker(object):
|
||||
device.stale(now):
|
||||
self.hass.async_add_job(device.async_update_ha_state(True))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_tracked_device(self):
|
||||
"""Setup all not exists tracked devices.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def async_init_single_device(dev):
|
||||
"""Init a single device_tracker entity."""
|
||||
yield from dev.async_added_to_hass()
|
||||
yield from dev.async_update_ha_state()
|
||||
|
||||
tasks = []
|
||||
for device in self.devices.values():
|
||||
if device.track and not device.last_seen:
|
||||
tasks.append(self.hass.async_add_job(
|
||||
async_init_single_device(device)))
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=self.hass.loop)
|
||||
|
||||
|
||||
class Device(Entity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
@@ -110,7 +110,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
_LOGGER.info("Discovered Bluetooth LE device %s", address)
|
||||
see_device(address, devs[address], new_device=True)
|
||||
|
||||
track_point_in_utc_time(hass, update_ble, now + interval)
|
||||
track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval)
|
||||
|
||||
update_ble(dt_util.utcnow())
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
see_device((mac, result))
|
||||
except bluetooth.BluetoothError:
|
||||
_LOGGER.exception('Error looking up bluetooth device!')
|
||||
track_point_in_utc_time(hass, update_bluetooth, now + interval)
|
||||
track_point_in_utc_time(
|
||||
hass, update_bluetooth, dt_util.utcnow() + interval)
|
||||
|
||||
update_bluetooth(dt_util.utcnow())
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.6']
|
||||
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
@@ -197,13 +197,22 @@ class Icloud(DeviceScanner):
|
||||
|
||||
def icloud_trusted_device_callback(self, callback_data):
|
||||
"""The trusted device is chosen."""
|
||||
self._trusted_device = int(callback_data.get('0', '0'))
|
||||
self._trusted_device = int(callback_data.get('trusted_device'))
|
||||
self._trusted_device = self.api.trusted_devices[self._trusted_device]
|
||||
|
||||
if not self.api.send_verification_code(self._trusted_device):
|
||||
_LOGGER.error('Failed to send verification code')
|
||||
self._trusted_device = None
|
||||
return
|
||||
|
||||
if self.accountname in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(self.accountname)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
|
||||
# Trigger the next step immediately
|
||||
self.icloud_need_verification_code()
|
||||
|
||||
def icloud_need_trusted_device(self):
|
||||
"""We need a trusted device."""
|
||||
configurator = get_component('configurator')
|
||||
@@ -213,7 +222,10 @@ class Icloud(DeviceScanner):
|
||||
devicesstring = ''
|
||||
devices = self.api.trusted_devices
|
||||
for i, device in enumerate(devices):
|
||||
devicesstring += "{}: {};".format(i, device.get('deviceName'))
|
||||
devicename = device.get(
|
||||
'deviceName',
|
||||
'SMS to %s' % device.get('phoneNumber'))
|
||||
devicesstring += "{}: {};".format(i, devicename)
|
||||
|
||||
_CONFIGURING[self.accountname] = configurator.request_config(
|
||||
self.hass, 'iCloud {}'.format(self.accountname),
|
||||
@@ -223,12 +235,27 @@ class Icloud(DeviceScanner):
|
||||
' the index from this list: ' + devicesstring),
|
||||
entity_picture="/static/images/config_icloud.png",
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': '0'}]
|
||||
fields=[{'id': 'trusted_device', 'name': 'Trusted Device'}]
|
||||
)
|
||||
|
||||
def icloud_verification_callback(self, callback_data):
|
||||
"""The trusted device is chosen."""
|
||||
self._verification_code = callback_data.get('0')
|
||||
from pyicloud.exceptions import PyiCloudException
|
||||
self._verification_code = callback_data.get('code')
|
||||
|
||||
try:
|
||||
if not self.api.validate_verification_code(
|
||||
self._trusted_device, self._verification_code):
|
||||
raise PyiCloudException('Unknown failure')
|
||||
except PyiCloudException as error:
|
||||
# Reset to the inital 2FA state to allow the user to retry
|
||||
_LOGGER.error('Failed to verify verification code: %s', error)
|
||||
self._trusted_device = None
|
||||
self._verification_code = None
|
||||
|
||||
# Trigger the next step immediately
|
||||
self.icloud_need_trusted_device()
|
||||
|
||||
if self.accountname in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(self.accountname)
|
||||
configurator = get_component('configurator')
|
||||
@@ -240,22 +267,17 @@ class Icloud(DeviceScanner):
|
||||
if self.accountname in _CONFIGURING:
|
||||
return
|
||||
|
||||
if self.api.send_verification_code(self._trusted_device):
|
||||
self._verification_code = 'waiting'
|
||||
|
||||
_CONFIGURING[self.accountname] = configurator.request_config(
|
||||
self.hass, 'iCloud {}'.format(self.accountname),
|
||||
self.icloud_verification_callback,
|
||||
description=('Please enter the validation code:'),
|
||||
entity_picture="/static/images/config_icloud.png",
|
||||
submit_caption='Confirm',
|
||||
fields=[{'code': '0'}]
|
||||
fields=[{'id': 'code', 'name': 'code'}]
|
||||
)
|
||||
|
||||
def keep_alive(self, now):
|
||||
"""Keep the api alive."""
|
||||
from pyicloud.exceptions import PyiCloud2FARequiredError
|
||||
|
||||
if self.api is None:
|
||||
self.reset_account_icloud()
|
||||
|
||||
@@ -263,9 +285,8 @@ class Icloud(DeviceScanner):
|
||||
return
|
||||
|
||||
if self.api.requires_2fa:
|
||||
from pyicloud.exceptions import PyiCloudException
|
||||
try:
|
||||
self.api.authenticate()
|
||||
except PyiCloud2FARequiredError:
|
||||
if self._trusted_device is None:
|
||||
self.icloud_need_trusted_device()
|
||||
return
|
||||
@@ -274,12 +295,14 @@ class Icloud(DeviceScanner):
|
||||
self.icloud_need_verification_code()
|
||||
return
|
||||
|
||||
if self._verification_code == 'waiting':
|
||||
return
|
||||
self.api.authenticate()
|
||||
if self.api.requires_2fa:
|
||||
raise Exception('Unknown failure')
|
||||
|
||||
if self.api.validate_verification_code(
|
||||
self._trusted_device, self._verification_code):
|
||||
self._verification_code = None
|
||||
self._trusted_device = None
|
||||
self._verification_code = None
|
||||
except PyiCloudException as error:
|
||||
_LOGGER.error("Error setting up 2fa: %s", error)
|
||||
else:
|
||||
self.api.authenticate()
|
||||
|
||||
@@ -397,13 +420,13 @@ class Icloud(DeviceScanner):
|
||||
try:
|
||||
if devicename is not None:
|
||||
if devicename in self.devices:
|
||||
self.devices[devicename].update_icloud()
|
||||
self.devices[devicename].location()
|
||||
else:
|
||||
_LOGGER.error("devicename %s unknown for account %s",
|
||||
devicename, self._attrs[ATTR_ACCOUNTNAME])
|
||||
else:
|
||||
for device in self.devices:
|
||||
self.devices[device].update_icloud()
|
||||
self.devices[device].location()
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error('No iCloud Devices found!')
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
@@ -31,6 +32,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
class InvalidLuciTokenError(HomeAssistantError):
|
||||
"""When an invalid token is detected."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Luci scanner."""
|
||||
scanner = LuciDeviceScanner(config[DOMAIN])
|
||||
@@ -46,8 +53,9 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
|
||||
@@ -55,12 +63,15 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.token = _get_token(host, username, password)
|
||||
self.host = host
|
||||
self.refresh_token()
|
||||
|
||||
self.mac2name = None
|
||||
self.success_init = self.token is not None
|
||||
|
||||
def refresh_token(self):
|
||||
"""Get a new token."""
|
||||
self.token = _get_token(self.host, self.username, self.password)
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
@@ -98,8 +109,15 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
_LOGGER.info('Checking ARP')
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
|
||||
try:
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
except InvalidLuciTokenError:
|
||||
_LOGGER.info('Refreshing token')
|
||||
self.refresh_token()
|
||||
return False
|
||||
|
||||
if result:
|
||||
self.last_results = []
|
||||
for device_entry in result:
|
||||
@@ -116,6 +134,7 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
def _req_json_rpc(url, method, *args, **kwargs):
|
||||
"""Perform one JSON RPC operation."""
|
||||
data = json.dumps({'method': method, 'params': args})
|
||||
|
||||
try:
|
||||
res = requests.post(url, data=data, timeout=5, **kwargs)
|
||||
except requests.exceptions.Timeout:
|
||||
@@ -139,6 +158,10 @@ def _req_json_rpc(url, method, *args, **kwargs):
|
||||
"Failed to authenticate, "
|
||||
"please check your username and password")
|
||||
return
|
||||
elif res.status_code == 403:
|
||||
_LOGGER.error('Luci responded with a 403 Invalid token')
|
||||
raise InvalidLuciTokenError
|
||||
|
||||
else:
|
||||
_LOGGER.error('Invalid response from luci: %s', res)
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ Support for tracking MQTT enabled devices.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_DEVICES
|
||||
from homeassistant.components.mqtt import CONF_QOS
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
@@ -23,19 +25,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Setup the MQTT tracker."""
|
||||
devices = config[CONF_DEVICES]
|
||||
qos = config[CONF_QOS]
|
||||
|
||||
dev_id_lookup = {}
|
||||
|
||||
def device_tracker_message_received(topic, payload, qos):
|
||||
@callback
|
||||
def async_tracker_message_received(topic, payload, qos):
|
||||
"""MQTT message received."""
|
||||
see(dev_id=dev_id_lookup[topic], location_name=payload)
|
||||
hass.async_add_job(
|
||||
async_see(dev_id=dev_id_lookup[topic], location_name=payload))
|
||||
|
||||
for dev_id, topic in devices.items():
|
||||
dev_id_lookup[topic] = dev_id
|
||||
mqtt.subscribe(hass, topic, device_tracker_message_received, qos)
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, topic, async_tracker_message_received, qos)
|
||||
|
||||
return True
|
||||
|
||||
@@ -4,14 +4,15 @@ Support the OwnTracks platform.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.owntracks/
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import STATE_HOME
|
||||
@@ -19,6 +20,7 @@ from homeassistant.util import convert, slugify
|
||||
from homeassistant.components import zone as zone_comp
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
REQUIREMENTS = ['libnacl==1.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -30,16 +32,9 @@ 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'
|
||||
@@ -61,7 +56,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
def get_cipher():
|
||||
"""Return decryption function and length of key."""
|
||||
"""Return decryption function and length of key.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
|
||||
from libnacl.secret import SecretBox
|
||||
|
||||
@@ -71,13 +69,17 @@ def get_cipher():
|
||||
return (KEYLEN, decrypt)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""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)
|
||||
|
||||
mobile_beacons_active = defaultdict(list)
|
||||
regions_entered = defaultdict(list)
|
||||
|
||||
def decrypt_payload(topic, ciphertext):
|
||||
"""Decrypt encrypted payload."""
|
||||
try:
|
||||
@@ -154,7 +156,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
|
||||
return data
|
||||
|
||||
def owntracks_location_update(topic, payload, qos):
|
||||
@callback
|
||||
def async_owntracks_location_update(topic, payload, qos):
|
||||
"""MQTT message received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||
@@ -164,18 +167,17 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
|
||||
dev_id, kwargs = _parse_see_args(topic, data)
|
||||
|
||||
# Block updates if we're in a region
|
||||
with LOCK:
|
||||
if REGIONS_ENTERED[dev_id]:
|
||||
_LOGGER.debug(
|
||||
"location update ignored - inside region %s",
|
||||
REGIONS_ENTERED[-1])
|
||||
return
|
||||
if regions_entered[dev_id]:
|
||||
_LOGGER.debug(
|
||||
"location update ignored - inside region %s",
|
||||
regions_entered[-1])
|
||||
return
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
def owntracks_event_update(topic, payload, qos):
|
||||
@callback
|
||||
def async_owntracks_event_update(topic, payload, qos):
|
||||
"""MQTT event (geofences) received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||
@@ -199,67 +201,65 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
def enter_event():
|
||||
"""Execute enter event."""
|
||||
zone = hass.states.get("zone.{}".format(slugify(location)))
|
||||
with LOCK:
|
||||
if zone is None and data.get('t') == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location not in beacons:
|
||||
beacons.append(location)
|
||||
_LOGGER.info("Added beacon %s", location)
|
||||
else:
|
||||
# Normal region
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location not in regions:
|
||||
regions.append(location)
|
||||
_LOGGER.info("Enter region %s", location)
|
||||
_set_gps_from_zone(kwargs, location, zone)
|
||||
if zone is None and data.get('t') == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
beacons = mobile_beacons_active[dev_id]
|
||||
if location not in beacons:
|
||||
beacons.append(location)
|
||||
_LOGGER.info("Added beacon %s", location)
|
||||
else:
|
||||
# Normal region
|
||||
regions = regions_entered[dev_id]
|
||||
if location not in regions:
|
||||
regions.append(location)
|
||||
_LOGGER.info("Enter region %s", location)
|
||||
_set_gps_from_zone(kwargs, location, zone)
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
def leave_event():
|
||||
"""Execute leave event."""
|
||||
with LOCK:
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location in regions:
|
||||
regions.remove(location)
|
||||
new_region = regions[-1] if regions else None
|
||||
regions = regions_entered[dev_id]
|
||||
if location in regions:
|
||||
regions.remove(location)
|
||||
new_region = regions[-1] if regions else None
|
||||
|
||||
if new_region:
|
||||
# Exit to previous region
|
||||
zone = hass.states.get(
|
||||
"zone.{}".format(slugify(new_region)))
|
||||
_set_gps_from_zone(kwargs, new_region, zone)
|
||||
_LOGGER.info("Exit to %s", new_region)
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
if new_region:
|
||||
# Exit to previous region
|
||||
zone = hass.states.get(
|
||||
"zone.{}".format(slugify(new_region)))
|
||||
_set_gps_from_zone(kwargs, new_region, zone)
|
||||
_LOGGER.info("Exit to %s", new_region)
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
# Check for GPS accuracy
|
||||
valid_gps = True
|
||||
if 'acc' in data:
|
||||
if data['acc'] == 0.0:
|
||||
valid_gps = False
|
||||
_LOGGER.warning(
|
||||
'Ignoring GPS in region exit because accuracy'
|
||||
'is zero: %s',
|
||||
payload)
|
||||
if (max_gps_accuracy is not None and
|
||||
data['acc'] > max_gps_accuracy):
|
||||
valid_gps = False
|
||||
_LOGGER.info(
|
||||
'Ignoring GPS in region exit because expected '
|
||||
'GPS accuracy %s is not met: %s',
|
||||
max_gps_accuracy, payload)
|
||||
if valid_gps:
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
# Check for GPS accuracy
|
||||
valid_gps = True
|
||||
if 'acc' in data:
|
||||
if data['acc'] == 0.0:
|
||||
valid_gps = False
|
||||
_LOGGER.warning(
|
||||
'Ignoring GPS in region exit because accuracy'
|
||||
'is zero: %s',
|
||||
payload)
|
||||
if (max_gps_accuracy is not None and
|
||||
data['acc'] > max_gps_accuracy):
|
||||
valid_gps = False
|
||||
_LOGGER.info(
|
||||
'Ignoring GPS in region exit because expected '
|
||||
'GPS accuracy %s is not met: %s',
|
||||
max_gps_accuracy, payload)
|
||||
if valid_gps:
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
_LOGGER.info("Remove beacon %s", location)
|
||||
beacons = mobile_beacons_active[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
_LOGGER.info("Remove beacon %s", location)
|
||||
|
||||
if data['event'] == 'enter':
|
||||
enter_event()
|
||||
@@ -271,7 +271,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
data['event'])
|
||||
return
|
||||
|
||||
def owntracks_waypoint_update(topic, payload, qos):
|
||||
@callback
|
||||
def async_owntracks_waypoint_update(topic, payload, qos):
|
||||
"""List of waypoints published by a user."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typewaypoints
|
||||
@@ -298,36 +299,43 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
||||
zone_comp.ICON_IMPORT, False)
|
||||
zone.entity_id = entity_id
|
||||
zone.update_ha_state()
|
||||
hass.async_add_job(zone.async_update_ha_state())
|
||||
|
||||
def see_beacons(dev_id, kwargs_param):
|
||||
@callback
|
||||
def async_see_beacons(dev_id, kwargs_param):
|
||||
"""Set active beacons to the current location."""
|
||||
kwargs = kwargs_param.copy()
|
||||
# the battery state applies to the tracking device, not the beacon
|
||||
kwargs.pop('battery', None)
|
||||
for beacon in MOBILE_BEACONS_ACTIVE[dev_id]:
|
||||
for beacon in mobile_beacons_active[dev_id]:
|
||||
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
|
||||
kwargs['host_name'] = beacon
|
||||
see(**kwargs)
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
|
||||
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
||||
mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1)
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, LOCATION_TOPIC, async_owntracks_location_update, 1)
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, EVENT_TOPIC, async_owntracks_event_update, 1)
|
||||
|
||||
if waypoint_import:
|
||||
if waypoint_whitelist is None:
|
||||
mqtt.subscribe(hass, WAYPOINT_TOPIC.format('+', '+'),
|
||||
owntracks_waypoint_update, 1)
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, WAYPOINT_TOPIC.format('+', '+'),
|
||||
async_owntracks_waypoint_update, 1)
|
||||
else:
|
||||
for whitelist_user in waypoint_whitelist:
|
||||
mqtt.subscribe(hass, WAYPOINT_TOPIC.format(whitelist_user,
|
||||
'+'),
|
||||
owntracks_waypoint_update, 1)
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, WAYPOINT_TOPIC.format(whitelist_user, '+'),
|
||||
async_owntracks_waypoint_update, 1)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def parse_topic(topic, pretty=False):
|
||||
"""Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple."""
|
||||
"""Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
parts = topic.split('/')
|
||||
dev_id_format = ''
|
||||
if pretty:
|
||||
@@ -340,7 +348,10 @@ def parse_topic(topic, pretty=False):
|
||||
|
||||
|
||||
def _parse_see_args(topic, data):
|
||||
"""Parse the OwnTracks location parameters, into the format see expects."""
|
||||
"""Parse the OwnTracks location parameters, into the format see expects.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
(host_name, dev_id) = parse_topic(topic, False)
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
@@ -355,7 +366,10 @@ def _parse_see_args(topic, data):
|
||||
|
||||
|
||||
def _set_gps_from_zone(kwargs, location, zone):
|
||||
"""Set the see parameters from the zone parameters."""
|
||||
"""Set the see parameters from the zone parameters.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
if zone is not None:
|
||||
kwargs['gps'] = (
|
||||
zone.attributes['latitude'],
|
||||
|
||||
@@ -86,7 +86,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Update all the hosts on every interval time."""
|
||||
for host in hosts:
|
||||
host.update(see)
|
||||
track_point_in_utc_time(hass, update, now + interval)
|
||||
track_point_in_utc_time(hass, update, util.dt.utcnow() + interval)
|
||||
return True
|
||||
|
||||
return update(util.dt.utcnow())
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.3.3']
|
||||
REQUIREMENTS = ['pysnmp==4.3.4']
|
||||
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
|
||||
@@ -142,7 +142,7 @@ class TadoDeviceScanner(DeviceScanner):
|
||||
|
||||
# Find devices that have geofencing enabled, and are currently at home.
|
||||
for mobile_device in tado_json:
|
||||
if 'location' in mobile_device:
|
||||
if mobile_device.get('location'):
|
||||
if mobile_device['location']['atHome']:
|
||||
device_id = mobile_device['id']
|
||||
device_name = mobile_device['name']
|
||||
|
||||
@@ -13,13 +13,15 @@ import homeassistant.loader as loader
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import CONF_VERIFY_SSL
|
||||
|
||||
# Unifi package doesn't list urllib3 as a requirement
|
||||
REQUIREMENTS = ['urllib3', 'pyunifi==1.3']
|
||||
REQUIREMENTS = ['pyunifi==2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
CONF_SITE_ID = 'site_id'
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
|
||||
NOTIFICATION_ID = 'unifi_notification'
|
||||
NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
|
||||
@@ -29,7 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SITE_ID, default='default'): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PORT, default=8443): cv.port
|
||||
vol.Required(CONF_PORT, default=8443): cv.port,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
@@ -42,10 +45,12 @@ def get_scanner(hass, config):
|
||||
password = config[DOMAIN].get(CONF_PASSWORD)
|
||||
site_id = config[DOMAIN].get(CONF_SITE_ID)
|
||||
port = config[DOMAIN].get(CONF_PORT)
|
||||
verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
ctrl = Controller(host, username, password, port, 'v4', site_id)
|
||||
ctrl = Controller(host, username, password, port, version='v4',
|
||||
site_id=site_id, ssl_verify=verify_ssl)
|
||||
except urllib.error.HTTPError as ex:
|
||||
_LOGGER.error('Failed to connect to Unifi: %s', ex)
|
||||
persistent_notification.create(
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-digitalocean==1.10.1']
|
||||
REQUIREMENTS = ['python-digitalocean==1.11']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -6,20 +6,24 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
|
||||
Knows which components handle certain types, will make sure they are
|
||||
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==0.8.3']
|
||||
REQUIREMENTS = ['netdisco==0.9.2']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
SCAN_INTERVAL = timedelta(seconds=300)
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HASS_IOS_APP = 'hass_ios'
|
||||
@@ -42,24 +46,28 @@ SERVICE_HANDLERS = {
|
||||
'yeelight': ('light', 'yeelight'),
|
||||
'flux_led': ('light', 'flux_led'),
|
||||
'apple_tv': ('media_player', 'apple_tv'),
|
||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
}
|
||||
|
||||
CONF_IGNORE = 'ignore'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(DOMAIN): vol.Schema({
|
||||
vol.Optional(CONF_IGNORE, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(SERVICE_HANDLERS)])
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Start a discovery service."""
|
||||
logger = logging.getLogger(__name__)
|
||||
from netdisco.discovery import NetworkDiscovery
|
||||
|
||||
from netdisco.service import DiscoveryService
|
||||
logger = logging.getLogger(__name__)
|
||||
netdisco = NetworkDiscovery()
|
||||
already_discovered = set()
|
||||
|
||||
# Disable zeroconf logging, it spams
|
||||
logging.getLogger('zeroconf').setLevel(logging.CRITICAL)
|
||||
@@ -67,37 +75,62 @@ def setup(hass, config):
|
||||
# Platforms ignore by config
|
||||
ignored_platforms = config[DOMAIN][CONF_IGNORE]
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
def new_service_listener(service, info):
|
||||
@asyncio.coroutine
|
||||
def new_service_found(service, info):
|
||||
"""Called when a new service is found."""
|
||||
if service in ignored_platforms:
|
||||
logger.info("Ignoring service: %s %s", service, info)
|
||||
return
|
||||
|
||||
with lock:
|
||||
logger.info("Found new service: %s %s", service, info)
|
||||
comp_plat = SERVICE_HANDLERS.get(service)
|
||||
|
||||
comp_plat = SERVICE_HANDLERS.get(service)
|
||||
# We do not know how to handle this service.
|
||||
if not comp_plat:
|
||||
return
|
||||
|
||||
# We do not know how to handle this service.
|
||||
if not comp_plat:
|
||||
return
|
||||
discovery_hash = json.dumps([service, info], sort_keys=True)
|
||||
if discovery_hash in already_discovered:
|
||||
return
|
||||
|
||||
component, platform = comp_plat
|
||||
already_discovered.add(discovery_hash)
|
||||
|
||||
if platform is None:
|
||||
discover(hass, service, info, component, config)
|
||||
else:
|
||||
load_platform(hass, component, platform, info, config)
|
||||
logger.info("Found new service: %s %s", service, info)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def start_discovery(event):
|
||||
"""Start discovering."""
|
||||
netdisco = DiscoveryService(SCAN_INTERVAL)
|
||||
netdisco.add_listener(new_service_listener)
|
||||
netdisco.start()
|
||||
component, platform = comp_plat
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_discovery)
|
||||
if platform is None:
|
||||
yield from async_discover(hass, service, info, component, config)
|
||||
else:
|
||||
yield from async_load_platform(
|
||||
hass, component, platform, info, config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def scan_devices(_):
|
||||
"""Scan for devices."""
|
||||
results = yield from hass.loop.run_in_executor(
|
||||
None, _discover, netdisco)
|
||||
|
||||
for result in results:
|
||||
hass.async_add_job(new_service_found(*result))
|
||||
|
||||
async_track_point_in_utc_time(hass, scan_devices,
|
||||
dt_util.utcnow() + SCAN_INTERVAL)
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, scan_devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _discover(netdisco):
|
||||
"""Discover devices."""
|
||||
results = []
|
||||
try:
|
||||
netdisco.scan()
|
||||
|
||||
for disc in netdisco.discover():
|
||||
for service in netdisco.get_info(disc):
|
||||
results.append((disc, service))
|
||||
finally:
|
||||
netdisco.stop()
|
||||
|
||||
return results
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/nkgilley/python-ecobee-api/archive/'
|
||||
'4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6']
|
||||
'a4496b293956b2eac285305136a62ac78bef510d.zip#python-ecobee==0.0.7']
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -218,7 +218,7 @@ def async_setup(hass, config: dict):
|
||||
if not fan.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
fan.async_update_ha_state(True))
|
||||
if hasattr(fan, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
@@ -78,7 +78,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup MQTT fan platform."""
|
||||
yield from async_add_devices([MqttFan(
|
||||
async_add_devices([MqttFan(
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
|
||||
@@ -14,6 +14,8 @@ from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send, async_dispatcher_connect)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
@@ -26,6 +28,10 @@ SERVICE_START = 'start'
|
||||
SERVICE_STOP = 'stop'
|
||||
SERVICE_RESTART = 'restart'
|
||||
|
||||
SIGNAL_FFMPEG_START = 'ffmpeg.start'
|
||||
SIGNAL_FFMPEG_STOP = 'ffmpeg.stop'
|
||||
SIGNAL_FFMPEG_RESTART = 'ffmpeg.restart'
|
||||
|
||||
DATA_FFMPEG = 'ffmpeg'
|
||||
|
||||
CONF_INITIAL_STATE = 'initial_state'
|
||||
@@ -50,22 +56,25 @@ SERVICE_FFMPEG_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def start(hass, entity_id=None):
|
||||
@callback
|
||||
def async_start(hass, entity_id=None):
|
||||
"""Start a ffmpeg process on entity."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_START, data)
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_START, data))
|
||||
|
||||
|
||||
def stop(hass, entity_id=None):
|
||||
@callback
|
||||
def async_stop(hass, entity_id=None):
|
||||
"""Stop a ffmpeg process on entity."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_STOP, data)
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_STOP, data))
|
||||
|
||||
|
||||
def restart(hass, entity_id=None):
|
||||
@callback
|
||||
def async_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)
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RESTART, data))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -89,30 +98,12 @@ def async_setup(hass, config):
|
||||
"""Handle service ffmpeg process."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if entity_ids:
|
||||
devices = [device for device in manager.entities
|
||||
if device.entity_id in entity_ids]
|
||||
if service.service == SERVICE_START:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids)
|
||||
elif service.service == SERVICE_STOP:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids)
|
||||
else:
|
||||
devices = manager.entities
|
||||
|
||||
tasks = []
|
||||
for device in devices:
|
||||
if service.service == SERVICE_START:
|
||||
tasks.append(device.async_start_ffmpeg())
|
||||
elif service.service == SERVICE_STOP:
|
||||
tasks.append(device.async_stop_ffmpeg())
|
||||
else:
|
||||
tasks.append(device.async_restart_ffmpeg())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
tasks.clear()
|
||||
for device in devices:
|
||||
tasks.append(device.async_update_ha_state())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, async_service_handle,
|
||||
@@ -140,42 +131,12 @@ class FFmpegManager(object):
|
||||
self._cache = {}
|
||||
self._bin = ffmpeg_bin
|
||||
self._run_test = run_test
|
||||
self._entities = []
|
||||
|
||||
@property
|
||||
def binary(self):
|
||||
"""Return ffmpeg binary from config."""
|
||||
return self._bin
|
||||
|
||||
@property
|
||||
def entities(self):
|
||||
"""Return ffmpeg entities for services."""
|
||||
return self._entities
|
||||
|
||||
@callback
|
||||
def async_register_device(self, device):
|
||||
"""Register a ffmpeg process/device."""
|
||||
self._entities.append(device)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_shutdown(event):
|
||||
"""Stop ffmpeg process."""
|
||||
yield from device.async_stop_ffmpeg()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, async_shutdown)
|
||||
|
||||
# start on startup
|
||||
if device.initial_state:
|
||||
@asyncio.coroutine
|
||||
def async_start(event):
|
||||
"""Start ffmpeg process."""
|
||||
yield from device.async_start_ffmpeg()
|
||||
yield from device.async_update_ha_state()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, async_start)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_run_test(self, input_source):
|
||||
"""Run test on this input. TRUE is deactivate or run correct.
|
||||
@@ -208,6 +169,22 @@ class FFmpegBase(Entity):
|
||||
self.ffmpeg = None
|
||||
self.initial_state = initial_state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register dispatcher & events.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg)
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg)
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg)
|
||||
|
||||
# register start/stop
|
||||
self._async_register_events()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
@@ -218,22 +195,53 @@ class FFmpegBase(Entity):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_start_ffmpeg(self, entity_ids):
|
||||
"""Start a ffmpeg process.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_stop_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_stop_ffmpeg(self, entity_ids):
|
||||
"""Stop a ffmpeg process.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
return self.ffmpeg.close()
|
||||
if entity_ids is None or self.entity_id in entity_ids:
|
||||
yield from self.ffmpeg.close()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_restart_ffmpeg(self):
|
||||
"""Stop a ffmpeg process."""
|
||||
yield from self.async_stop_ffmpeg()
|
||||
yield from self.async_start_ffmpeg()
|
||||
def _async_restart_ffmpeg(self, entity_ids):
|
||||
"""Stop a ffmpeg process.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if entity_ids is None or self.entity_id in entity_ids:
|
||||
yield from self._async_stop_ffmpeg(None)
|
||||
yield from self._async_start_ffmpeg(None)
|
||||
|
||||
@callback
|
||||
def _async_register_events(self):
|
||||
"""Register a ffmpeg process/device."""
|
||||
@asyncio.coroutine
|
||||
def async_shutdown_handle(event):
|
||||
"""Stop ffmpeg process."""
|
||||
yield from self._async_stop_ffmpeg(None)
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, async_shutdown_handle)
|
||||
|
||||
# start on startup
|
||||
if not self.initial_state:
|
||||
return
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_start_handle(event):
|
||||
"""Start ffmpeg process."""
|
||||
yield from self._async_start_ffmpeg(None)
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, async_start_handle)
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
window.Polymer = {
|
||||
lazyRegister: true,
|
||||
useNativeCSSProperties: true,
|
||||
dom: 'shady',
|
||||
dom: 'shadow',
|
||||
suppressTemplateNotifications: true,
|
||||
suppressBindingNotifications: true,
|
||||
};
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
||||
"core.js": "1f7f88d8f5dada08bce1d935cfa5f33e",
|
||||
"frontend.html": "ca9efa7e4506aa6b1a668703c8d0f800",
|
||||
"mdi.html": "c1dde43ccf5667f687c418fc8daf9668",
|
||||
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
|
||||
"frontend.html": "53c45b837a3bcae7cfb9ef4a5919844f",
|
||||
"mdi.html": "4921d26f29dc148c3e8bd5bcd8ce5822",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057",
|
||||
"panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4",
|
||||
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
|
||||
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
|
||||
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
|
||||
"panels/ha-panel-dev-service.html": "a9247f255174b084fad2c04bdb9ec7a9",
|
||||
"panels/ha-panel-dev-state.html": "90f3bede9602241552ef7bb7958198c6",
|
||||
"panels/ha-panel-dev-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb",
|
||||
"panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50",
|
||||
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
|
||||
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
|
||||
"panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae",
|
||||
"panels/ha-panel-history.html": "bfd5f929d5aa9cefdd799ec37624efa1",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "2af1feb30b37427f481d5437a438a3f2",
|
||||
"panels/ha-panel-map.html": "e10704a3469e44d1714eac9ed8e4b6a0",
|
||||
"panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
|
||||
"panels/ha-panel-map.html": "9aa065b1908089f3bb5af7fdf9485be5",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
||||
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.
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.
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.
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user