Compare commits
255 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9252854f99 | |||
| 8d76e2679d | |||
| 4b1dcad7ae | |||
| b45c386fd6 | |||
| cb5fa79835 | |||
| b74217bec2 | |||
| fcf60e740d | |||
| 9b1ed4e79b | |||
| 8fffaebe50 | |||
| 84aab1c973 | |||
| a2fbc0d2ef | |||
| 363a429c41 | |||
| 9fc22ee47a | |||
| a250f583eb | |||
| bf495edbb5 | |||
| 3ea7dee83d | |||
| d796e8db5c | |||
| d24b45054a | |||
| d67f3b8060 | |||
| 4339e9aab1 | |||
| 9b640f6a81 | |||
| 437ddb8dea | |||
| a119bd0056 | |||
| 0eaad46d93 | |||
| 8af6bacfd0 | |||
| 09ca440c20 | |||
| 74cc675a38 | |||
| c478f2c7d0 | |||
| a3a702b269 | |||
| 92a6f21cc2 | |||
| 814834512a | |||
| 46f3088a70 | |||
| deed760008 | |||
| d1da53615f | |||
| 69c919183a | |||
| 8eb29787a5 | |||
| ae3973144c | |||
| 02f7eb9675 | |||
| 8c0967a190 | |||
| bf2fe60cb5 | |||
| 1ddcab5e26 | |||
| 09fec29537 | |||
| 9189cbdc8b | |||
| 7fae8cd0f1 | |||
| 843f8ce9ee | |||
| 2bf781185f | |||
| 1e1d4c2013 | |||
| bde711a9ff | |||
| dc45ed38e7 | |||
| 03f916ed10 | |||
| 6e33c12008 | |||
| 401309c3b2 | |||
| 1c06b51968 | |||
| e7de1fb9ae | |||
| de0f6b781e | |||
| 314bce1073 | |||
| 9e16be3173 | |||
| 1b1619fbf1 | |||
| 1f226cffe9 | |||
| b9ee5fb867 | |||
| ba80d5e52a | |||
| f2feabcf0b | |||
| a19e7ba3f1 | |||
| 49d642741d | |||
| db0efc647d | |||
| 640c692e1f | |||
| 4aef0b68bc | |||
| c2b7c93375 | |||
| 8cc759ea4b | |||
| a223efb840 | |||
| c32807803e | |||
| 24a172163a | |||
| 372169a03a | |||
| e4d100d54d | |||
| bfd9623d8b | |||
| 3464454662 | |||
| 533bb5565b | |||
| a8709a6988 | |||
| 4b767b088e | |||
| c52b18d7c8 | |||
| aaaf9637eb | |||
| 055db05946 | |||
| 0863d50210 | |||
| 1e352d37d0 | |||
| 620197b276 | |||
| 727a22f925 | |||
| 9bea7d7d8b | |||
| 97f62cfb78 | |||
| 482db94372 | |||
| 8a4e993183 | |||
| 790610525b | |||
| 7e668ef9e3 | |||
| 4dbf7be267 | |||
| 36eb0ceff3 | |||
| d38acfbd39 | |||
| b87e31617a | |||
| bb6fe822f9 | |||
| 5504a511e3 | |||
| 5c96936eb4 | |||
| cbbb15fa48 | |||
| 760138ac52 | |||
| b1f538b622 | |||
| ac8592587f | |||
| aee25a020d | |||
| 13df925795 | |||
| 2b850f417e | |||
| f303f6a191 | |||
| f8cfa15152 | |||
| 12f731b32c | |||
| 11dcbd4449 | |||
| fa6a089fb3 | |||
| 87da2ff1d7 | |||
| b576df53e9 | |||
| b90964faad | |||
| 549133a062 | |||
| c29553517f | |||
| 2e27c0d5ec | |||
| 774f584ba8 | |||
| 81b1446aad | |||
| 6bfd52ada8 | |||
| 0646d01152 | |||
| da5f5335eb | |||
| c9d55cff23 | |||
| aeb1d3d3fe | |||
| a1c119adb6 | |||
| e9f273e7e0 | |||
| 7ebf36bb70 | |||
| 84fe4f75df | |||
| c07bf551d9 | |||
| a745bf83ef | |||
| 1432ae649a | |||
| cf1a27bd7c | |||
| 3d8b7a4122 | |||
| e50588afe1 | |||
| 4dc4a98caa | |||
| 423e809e45 | |||
| a79f1d4d40 | |||
| 8461cf2717 | |||
| 9c9f5068b7 | |||
| 6d41024e76 | |||
| 7d24efc690 | |||
| 7d4adbbef5 | |||
| e11ec88482 | |||
| e39bdf8763 | |||
| a33bcdf270 | |||
| f056cbc641 | |||
| 4163bcebbc | |||
| d472d81538 | |||
| 2b70b1881a | |||
| 12607aeaea | |||
| 1855f1ae85 | |||
| 613da308f2 | |||
| cefacf9ce4 | |||
| 78887c5d5c | |||
| 3a92bd78ea | |||
| d0021a6171 | |||
| e2cfdbff06 | |||
| 9480f41210 | |||
| 1b5f6aa1b9 | |||
| 2065426b16 | |||
| beb8c05d91 | |||
| cf42303afb | |||
| 4bcbeef480 | |||
| e0712ba329 | |||
| 66d6f5174d | |||
| 9762e1613d | |||
| bb92ef5497 | |||
| 9f5bfe28d1 | |||
| 8ee32a8fbd | |||
| 052cd3fc53 | |||
| 0ccaf97924 | |||
| 96b20b3a97 | |||
| 91806bfa2a | |||
| 1c4e097bed | |||
| 2df6aabbf3 | |||
| 81b2111751 | |||
| f7e0d13fe6 | |||
| 5e5c0daa87 | |||
| a7277db4d7 | |||
| ba44b7edb3 | |||
| 8fcc750998 | |||
| eff619a58f | |||
| fc1bb58247 | |||
| c12b8f763c | |||
| ef51d8518a | |||
| 8b7894fb86 | |||
| 010f098df3 | |||
| 1f3bb51821 | |||
| 10367eb250 | |||
| 7fb5488058 | |||
| e68bd0457c | |||
| 910020bc5f | |||
| f43db3c615 | |||
| 9e9705d6b2 | |||
| 6899c7b6f7 | |||
| d0c9d6b69a | |||
| 81aaeaaf11 | |||
| 65c3201fa6 | |||
| 3a843e1817 | |||
| 0c7f8e910e | |||
| 0abde3aa57 | |||
| 775d45ae5a | |||
| e7d783ca2a | |||
| ef4ef2d383 | |||
| 3638b21bcb | |||
| 54c45f80c1 | |||
| e3307fb1c2 | |||
| b5f20c9b64 | |||
| 7055fddfb4 | |||
| fce09f624b | |||
| be53cc7068 | |||
| f3dabe21ab | |||
| 228fb8c072 | |||
| c556b619b7 | |||
| 2682996939 | |||
| 6872daab89 | |||
| 6d183e8bb3 | |||
| cdc8628e5a | |||
| dc4b0695b5 | |||
| 3fb691ead6 | |||
| a9926e355f | |||
| 17cbe0c6ce | |||
| 783abc7996 | |||
| 47355eed41 | |||
| d5642a5faf | |||
| ca3f07cdef | |||
| 99ea1e3f4f | |||
| bb8de5845a | |||
| b3cb057aac | |||
| 922303fd4b | |||
| 8c1181f8e3 | |||
| 4a0d6e73f4 | |||
| 171086229a | |||
| 927024714b | |||
| 24b7fd3694 | |||
| d6f43ba839 | |||
| 3492545ec1 | |||
| ceff9981be | |||
| 44edf3e105 | |||
| 81f0826550 | |||
| adde9e6231 | |||
| f637a07016 | |||
| 9e153119ef | |||
| b5c54864ac | |||
| d369d70ca5 | |||
| 5aa72562a7 | |||
| 7daa92249a | |||
| e91fe94585 | |||
| 88ffe39945 | |||
| e479324db9 | |||
| f65cc68705 | |||
| 238921b681 | |||
| 0fd415d7fb | |||
| 0eb6540fe7 | |||
| fc0c8540d3 |
+26
-1
@@ -20,6 +20,9 @@ omit =
|
||||
homeassistant/components/android_ip_webcam.py
|
||||
homeassistant/components/*/android_ip_webcam.py
|
||||
|
||||
homeassistant/components/arlo.py
|
||||
homeassistant/components/*/arlo.py
|
||||
|
||||
homeassistant/components/axis.py
|
||||
homeassistant/components/*/axis.py
|
||||
|
||||
@@ -62,6 +65,9 @@ omit =
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
homeassistant/components/juicenet.py
|
||||
homeassistant/components/*/juicenet.py
|
||||
|
||||
homeassistant/components/kira.py
|
||||
homeassistant/components/*/kira.py
|
||||
|
||||
@@ -71,6 +77,9 @@ omit =
|
||||
homeassistant/components/lutron_caseta.py
|
||||
homeassistant/components/*/lutron_caseta.py
|
||||
|
||||
homeassistant/components/mailgun.py
|
||||
homeassistant/components/*/mailgun.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
@@ -89,6 +98,9 @@ omit =
|
||||
homeassistant/components/qwikswitch.py
|
||||
homeassistant/components/*/qwikswitch.py
|
||||
|
||||
homeassistant/components/rachio.py
|
||||
homeassistant/components/*/rachio.py
|
||||
|
||||
homeassistant/components/raspihats.py
|
||||
homeassistant/components/*/raspihats.py
|
||||
|
||||
@@ -191,6 +203,7 @@ omit =
|
||||
homeassistant/components/binary_sensor/pilight.py
|
||||
homeassistant/components/binary_sensor/ping.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
@@ -198,8 +211,10 @@ omit =
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/flexit.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/knx.py
|
||||
@@ -280,6 +295,7 @@ omit =
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/apple_tv.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
@@ -304,6 +320,7 @@ omit =
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/nad.py
|
||||
homeassistant/components/media_player/nadtcp.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/openhome.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
@@ -335,7 +352,6 @@ omit =
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/mailgun.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
@@ -364,8 +380,10 @@ omit =
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/blockchain.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
@@ -385,6 +403,7 @@ omit =
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.py
|
||||
homeassistant/components/sensor/envirophat.py
|
||||
homeassistant/components/sensor/etherscan.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/fido.py
|
||||
@@ -392,6 +411,7 @@ omit =
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gitter.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
@@ -429,6 +449,8 @@ omit =
|
||||
homeassistant/components/sensor/pushbullet.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
@@ -458,6 +480,7 @@ omit =
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/zamg.py
|
||||
homeassistant/components/spc.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
homeassistant/components/switch/arest.py
|
||||
@@ -486,8 +509,10 @@ omit =
|
||||
homeassistant/components/tts/picotts.py
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/buienradar.py
|
||||
homeassistant/components/weather/metoffice.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/yweather.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/util.py
|
||||
|
||||
+1
-1
@@ -5,10 +5,10 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
#ENV INSTALL_TELLSTICK no
|
||||
#ENV INSTALL_OPENALPR no
|
||||
#ENV INSTALL_FFMPEG no
|
||||
#ENV INSTALL_OPENZWAVE no
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_COAP_CLIENT no
|
||||
#ENV INSTALL_SSOCR no
|
||||
|
||||
VOLUME /config
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<ul>
|
||||
<li><a href="https://community.home-assistant.io">📌 Community Forums</a></li>
|
||||
<li><a href="https://github.com/home-assistant/home-assistant">🚀 GitHub</a></li>
|
||||
<li><a href="https://home-assistant.io/">🏡 Homepage</a></li>
|
||||
<li><a href="https://gitter.im/home-assistant/home-assistant">💬 Gitter</a></li>
|
||||
<li><a href="https://pypi.python.org/pypi/homeassistant">💾 Download Releases</a></li>
|
||||
<li><a href="https://home-assistant.io/">Homepage</a></li>
|
||||
<li><a href="https://community.home-assistant.io">Community Forums</a></li>
|
||||
<li><a href="https://github.com/home-assistant/home-assistant">GitHub</a></li>
|
||||
<li><a href="https://gitter.im/home-assistant/home-assistant">Gitter</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
|
||||
+14
-51
@@ -10,6 +10,7 @@ import threading
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
from homeassistant import monkey_patch
|
||||
from homeassistant.const import (
|
||||
__version__,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
@@ -17,7 +18,6 @@ from homeassistant.const import (
|
||||
REQUIRED_PYTHON_VER_WIN,
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
|
||||
def attempt_use_uvloop():
|
||||
@@ -31,50 +31,8 @@ def attempt_use_uvloop():
|
||||
pass
|
||||
|
||||
|
||||
def monkey_patch_asyncio():
|
||||
"""Replace weakref.WeakSet to address Python 3 bug.
|
||||
|
||||
Under heavy threading operations that schedule calls into
|
||||
the asyncio event loop, Task objects are created. Due to
|
||||
a bug in Python, GC may have an issue when switching between
|
||||
the threads and objects with __del__ (which various components
|
||||
in HASS have).
|
||||
|
||||
This monkey-patch removes the weakref.Weakset, and replaces it
|
||||
with an object that ignores the only call utilizing it (the
|
||||
Task.__init__ which calls _all_tasks.add(self)). It also removes
|
||||
the __del__ which could trigger the future objects __del__ at
|
||||
unpredictable times.
|
||||
|
||||
The side-effect of this manipulation of the Task is that
|
||||
Task.all_tasks() is no longer accurate, and there will be no
|
||||
warning emitted if a Task is GC'd while in use.
|
||||
|
||||
On Python 3.6, after the bug is fixed, this monkey-patch can be
|
||||
disabled.
|
||||
|
||||
See https://bugs.python.org/issue26617 for details of the Python
|
||||
bug.
|
||||
"""
|
||||
# pylint: disable=no-self-use, protected-access, bare-except
|
||||
import asyncio.tasks
|
||||
|
||||
class IgnoreCalls:
|
||||
"""Ignore add calls."""
|
||||
|
||||
def add(self, other):
|
||||
"""No-op add."""
|
||||
return
|
||||
|
||||
asyncio.tasks.Task._all_tasks = IgnoreCalls()
|
||||
try:
|
||||
del asyncio.tasks.Task.__del__
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def validate_python() -> None:
|
||||
"""Validate we're running the right Python version."""
|
||||
"""Validate that the right Python version is running."""
|
||||
if sys.platform == "win32" and \
|
||||
sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
@@ -215,7 +173,7 @@ def daemonize() -> None:
|
||||
|
||||
|
||||
def check_pid(pid_file: str) -> None:
|
||||
"""Check that HA is not already running."""
|
||||
"""Check that Home Assistant is not already running."""
|
||||
# Check pid file
|
||||
try:
|
||||
pid = int(open(pid_file, 'r').readline())
|
||||
@@ -310,6 +268,9 @@ def setup_and_run_hass(config_dir: str,
|
||||
return None
|
||||
|
||||
if args.open_ui:
|
||||
# Imported here to avoid importing asyncio before monkey patch
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
def open_browser(event):
|
||||
"""Open the webinterface in a browser."""
|
||||
if hass.config.api is not None:
|
||||
@@ -326,7 +287,7 @@ def setup_and_run_hass(config_dir: str,
|
||||
|
||||
|
||||
def try_to_restart() -> None:
|
||||
"""Attempt to clean up state and start a new homeassistant instance."""
|
||||
"""Attempt to clean up state and start a new Home Assistant instance."""
|
||||
# Things should be mostly shut down already at this point, now just try
|
||||
# to clean up things that may have been left behind.
|
||||
sys.stderr.write('Home Assistant attempting to restart.\n')
|
||||
@@ -358,11 +319,11 @@ def try_to_restart() -> None:
|
||||
else:
|
||||
os.closerange(3, max_fd)
|
||||
|
||||
# Now launch into a new instance of Home-Assistant. If this fails we
|
||||
# Now launch into a new instance of Home Assistant. If this fails we
|
||||
# fall through and exit with error 100 (RESTART_EXIT_CODE) in which case
|
||||
# systemd will restart us when RestartForceExitStatus=100 is set in the
|
||||
# systemd.service file.
|
||||
sys.stderr.write("Restarting Home-Assistant\n")
|
||||
sys.stderr.write("Restarting Home Assistant\n")
|
||||
args = cmdline()
|
||||
os.execv(args[0], args)
|
||||
|
||||
@@ -371,10 +332,12 @@ def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
attempt_use_uvloop()
|
||||
if os.environ.get('HASS_NO_MONKEY') != '1':
|
||||
if sys.version_info[:2] >= (3, 6):
|
||||
monkey_patch.disable_c_asyncio()
|
||||
monkey_patch.patch_weakref_tasks()
|
||||
|
||||
if sys.version_info[:3] < (3, 5, 3):
|
||||
monkey_patch_asyncio()
|
||||
attempt_use_uvloop()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
|
||||
@@ -83,8 +83,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, conf_util.process_ha_config_upgrade, hass)
|
||||
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
if enable_log:
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
@@ -95,7 +94,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
'This may cause issues.')
|
||||
|
||||
if not loader.PREPARED:
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
yield from hass.async_add_job(loader.prepare, hass)
|
||||
|
||||
# Merge packages
|
||||
conf_util.merge_packages_config(
|
||||
@@ -184,14 +183,13 @@ def async_from_config_file(config_path: str,
|
||||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, mount_local_lib_path, config_dir)
|
||||
yield from hass.async_add_job(mount_local_lib_path, config_dir)
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
try:
|
||||
config_dict = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, config_path)
|
||||
config_dict = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, config_path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Error loading %s: %s', config_path, err)
|
||||
return None
|
||||
|
||||
@@ -123,8 +123,8 @@ def async_setup(hass, config):
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
@@ -158,8 +158,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.alarm_disarm, code)
|
||||
return self.hass.async_add_job(self.alarm_disarm, code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
@@ -170,8 +169,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.alarm_arm_home, code)
|
||||
return self.hass.async_add_job(self.alarm_arm_home, code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
@@ -182,8 +180,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.alarm_arm_away, code)
|
||||
return self.hass.async_add_job(self.alarm_arm_away, code)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command."""
|
||||
@@ -194,8 +191,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.alarm_trigger, code)
|
||||
return self.hass.async_add_job(self.alarm_trigger, code)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
|
||||
@@ -117,7 +117,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('home')
|
||||
self._alarm.arm('stay')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
|
||||
@@ -70,8 +70,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
device.async_alarm_keypress(keypress)
|
||||
|
||||
# Register Envisalink specific services
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Support for Vanderbilt (formerly Siemens) SPC alarm systems.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.spc/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.spc import (
|
||||
SpcWebGateway, ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPC_AREA_MODE_TO_STATE = {'0': STATE_ALARM_DISARMED,
|
||||
'1': STATE_ALARM_ARMED_HOME,
|
||||
'3': STATE_ALARM_ARMED_AWAY}
|
||||
|
||||
|
||||
def _get_alarm_state(spc_mode):
|
||||
return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the SPC alarm control panel platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
||||
return
|
||||
|
||||
entities = [SpcAlarm(hass=hass,
|
||||
area_id=area['id'],
|
||||
name=area['name'],
|
||||
state=_get_alarm_state(area['mode']))
|
||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SpcAlarm(alarm.AlarmControlPanel):
|
||||
"""Represents the SPC alarm panel."""
|
||||
|
||||
def __init__(self, hass, area_id, name, state):
|
||||
"""Initialize the SPC alarm panel."""
|
||||
self._hass = hass
|
||||
self._area_id = area_id
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._api = hass.data[DATA_API]
|
||||
|
||||
hass.data[DATA_REGISTRY].register_alarm_device(area_id, self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state):
|
||||
"""Update the alarm panel with a new state."""
|
||||
self._state = state
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_SET)
|
||||
@@ -128,8 +128,8 @@ def async_setup(hass, config):
|
||||
all_alerts[entity.entity_id] = entity
|
||||
|
||||
# Read descriptions
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = descriptions.get(DOMAIN, {})
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['apcaccess==0.0.4']
|
||||
REQUIREMENTS = ['apcaccess==0.0.10']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class APIEventStream(HomeAssistantView):
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
|
||||
restrict = request.GET.get('restrict')
|
||||
restrict = request.query.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
This component provides basic support for Netgear Arlo IP cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/arlo/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.loader as loader
|
||||
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com'
|
||||
|
||||
DOMAIN = 'arlo'
|
||||
|
||||
DEFAULT_BRAND = 'Netgear Arlo'
|
||||
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Camera Setup'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up an Arlo component."""
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
from pyarlo import PyArlo
|
||||
|
||||
arlo = PyArlo(username, password, preload=False)
|
||||
if not arlo.is_connected:
|
||||
return False
|
||||
hass.data['arlo'] = arlo
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
return True
|
||||
@@ -29,6 +29,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
|
||||
DOMAIN = 'automation'
|
||||
DEPENDENCIES = ['group']
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||
@@ -158,8 +159,8 @@ def async_setup(hass, config):
|
||||
|
||||
yield from _async_process_config(hass, config, component)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, os.path.join(
|
||||
descriptions = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_point_in_utc_time)
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ENTITY_ID = 'entity_id'
|
||||
@@ -40,10 +41,11 @@ def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
||||
to_state = get_deprecated(config, CONF_TO, CONF_STATE, MATCH_ALL)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_state_for_listener = None
|
||||
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
||||
|
||||
@callback
|
||||
def clear_listener():
|
||||
@@ -75,12 +77,13 @@ def async_trigger(hass, config, action):
|
||||
}
|
||||
})
|
||||
|
||||
if time_delta is None:
|
||||
call_action()
|
||||
# Ignore changes to state attributes if from/to is in use
|
||||
if (not match_all and from_s is not None and to_s is not None and
|
||||
from_s.last_changed == to_s.last_changed):
|
||||
return
|
||||
|
||||
# If only state attributes changed, ignore this event
|
||||
if from_s.last_changed == to_s.last_changed:
|
||||
if time_delta is None:
|
||||
call_action()
|
||||
return
|
||||
|
||||
@callback
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
|
||||
from homeassistant.const import CONF_AT, CONF_PLATFORM, CONF_AFTER
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
@@ -22,20 +22,26 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'time',
|
||||
CONF_AT: cv.time,
|
||||
CONF_AFTER: cv.time,
|
||||
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES,
|
||||
CONF_SECONDS, CONF_AFTER))
|
||||
CONF_SECONDS, CONF_AT, CONF_AFTER))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AFTER in config:
|
||||
after = config.get(CONF_AFTER)
|
||||
hours, minutes, seconds = after.hour, after.minute, after.second
|
||||
if CONF_AT in config:
|
||||
at_time = config.get(CONF_AT)
|
||||
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
||||
elif CONF_AFTER in config:
|
||||
_LOGGER.warning("'after' is deprecated for the time trigger. Please "
|
||||
"rename 'after' to 'at' in your configuration file.")
|
||||
at_time = config.get(CONF_AFTER)
|
||||
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
||||
else:
|
||||
hours = config.get(CONF_HOURS)
|
||||
minutes = config.get(CONF_MINUTES)
|
||||
|
||||
@@ -80,6 +80,12 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
elif value2 == 0x10:
|
||||
self.which = 1
|
||||
self.onoff = 1
|
||||
elif value2 == 0x37:
|
||||
self.which = 10
|
||||
self.onoff = 0
|
||||
elif value2 == 0x15:
|
||||
self.which = 10
|
||||
self.onoff = 1
|
||||
self.hass.bus.fire('button_pressed', {'id': self.dev_id,
|
||||
'pushed': value,
|
||||
'which': self.which,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for Homematic binary sensors.
|
||||
Support for HomeMatic binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematic/
|
||||
@@ -29,7 +29,7 @@ SENSOR_TYPES_CLASS = {
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Homematic binary sensor platform."""
|
||||
"""Set up the HomeMatic binary sensor platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
@@ -43,7 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
"""Representation of a binary Homematic device."""
|
||||
"""Representation of a binary HomeMatic device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -54,16 +54,14 @@ class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
# If state is MOTION (RemoteMotion works only)
|
||||
"""Return the class of this sensor from DEVICE_CLASSES."""
|
||||
# If state is MOTION (Only RemoteMotion working)
|
||||
if self._state == 'MOTION':
|
||||
return 'motion'
|
||||
return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None)
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data struct (self._data) from the Homematic metadata."""
|
||||
# add state to data struct
|
||||
"""Generate the data dictionary (self._data) from metadata."""
|
||||
# Add state to data struct
|
||||
if self._state:
|
||||
_LOGGER.debug("%s init datastruct with main node '%s'", self._name,
|
||||
self._state)
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
|
||||
@@ -38,7 +38,7 @@ class MyStromView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""The GET request received from a myStrom button."""
|
||||
res = yield from self._handle(request.app['hass'], request.GET)
|
||||
res = yield from self._handle(request.app['hass'], request.query)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Support for Vanderbilt (formerly Siemens) SPC alarm systems.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.spc/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.spc import (
|
||||
ATTR_DISCOVER_DEVICES, DATA_REGISTRY)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import (STATE_UNAVAILABLE, STATE_ON, STATE_OFF)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPC_TYPE_TO_DEVICE_CLASS = {'0': 'motion',
|
||||
'1': 'opening',
|
||||
'3': 'smoke'}
|
||||
|
||||
|
||||
SPC_INPUT_TO_SENSOR_STATE = {'0': STATE_OFF,
|
||||
'1': STATE_ON}
|
||||
|
||||
|
||||
def _get_device_class(spc_type):
|
||||
return SPC_TYPE_TO_DEVICE_CLASS.get(spc_type, None)
|
||||
|
||||
|
||||
def _get_sensor_state(spc_input):
|
||||
return SPC_INPUT_TO_SENSOR_STATE.get(spc_input, STATE_UNAVAILABLE)
|
||||
|
||||
|
||||
def _create_sensor(hass, zone):
|
||||
return SpcBinarySensor(zone_id=zone['id'],
|
||||
name=zone['zone_name'],
|
||||
state=_get_sensor_state(zone['input']),
|
||||
device_class=_get_device_class(zone['type']),
|
||||
spc_registry=hass.data[DATA_REGISTRY])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Initialize the platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
_create_sensor(hass, zone)
|
||||
for zone in discovery_info[ATTR_DISCOVER_DEVICES]
|
||||
if _get_device_class(zone['type']))
|
||||
|
||||
|
||||
class SpcBinarySensor(BinarySensorDevice):
|
||||
"""Represents a sensor based on an SPC zone."""
|
||||
|
||||
def __init__(self, zone_id, name, state, device_class, spc_registry):
|
||||
"""Initialize the sensor device."""
|
||||
self._zone_id = zone_id
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._device_class = device_class
|
||||
|
||||
spc_registry.register_sensor_device(zone_id, self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state):
|
||||
"""Update the state of the device."""
|
||||
self._state = state
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Whether the device is switched on."""
|
||||
return self._state == STATE_ON
|
||||
|
||||
@property
|
||||
def hidden(self) -> bool:
|
||||
"""Whether the device is hidden by default."""
|
||||
# these type of sensors are probably mainly used for automations
|
||||
return True
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""The device class."""
|
||||
return self._device_class
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Support for Taps Affs.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.tapsaff/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['tapsaff==0.1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_LOCATION = 'location'
|
||||
|
||||
DEFAULT_NAME = 'Taps Aff'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=30)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_LOCATION): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Taps Aff binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
location = config.get(CONF_LOCATION)
|
||||
|
||||
taps_aff_data = TapsAffData(location)
|
||||
|
||||
add_devices([TapsAffSensor(taps_aff_data, name)], True)
|
||||
|
||||
|
||||
class TapsAffSensor(BinarySensorDevice):
|
||||
"""Implementation of a Taps Aff binary sensor."""
|
||||
|
||||
def __init__(self, taps_aff_data, name):
|
||||
"""Initialize the Taps Aff sensor."""
|
||||
self.data = taps_aff_data
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return '{}'.format(self._name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if taps aff."""
|
||||
return self.data.is_taps_aff
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
self.data.update()
|
||||
|
||||
|
||||
class TapsAffData(object):
|
||||
"""Class for handling the data retrieval for pins."""
|
||||
|
||||
def __init__(self, location):
|
||||
"""Initialize the sensor."""
|
||||
from tapsaff import TapsAff
|
||||
|
||||
self._is_taps_aff = None
|
||||
self.taps_aff = TapsAff(location)
|
||||
|
||||
@property
|
||||
def is_taps_aff(self):
|
||||
"""Return true if taps aff."""
|
||||
return self._is_taps_aff
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from the Taps Aff API and updates the states."""
|
||||
try:
|
||||
self._is_taps_aff = self.taps_aff.is_taps_aff
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Update failed. Check configured location")
|
||||
@@ -1,82 +1,82 @@
|
||||
"""
|
||||
Demo platform that has two fake binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo Calendar platform."""
|
||||
calendar_data_future = DemoGoogleCalendarDataFuture()
|
||||
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
||||
add_devices([
|
||||
DemoGoogleCalendar(hass, calendar_data_future, {
|
||||
CONF_NAME: 'Future Event',
|
||||
CONF_DEVICE_ID: 'future_event',
|
||||
}),
|
||||
|
||||
DemoGoogleCalendar(hass, calendar_data_current, {
|
||||
CONF_NAME: 'Current Event',
|
||||
CONF_DEVICE_ID: 'current_event',
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
class DemoGoogleCalendarData(object):
|
||||
"""Representation of a Demo Calendar element."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def update(self):
|
||||
"""Return true so entity knows we have new data."""
|
||||
return True
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
||||
"""Representation of a Demo Calendar for a future event."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event to a future event."""
|
||||
one_hour_from_now = dt_util.now() \
|
||||
+ dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': one_hour_from_now.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (one_hour_from_now + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Future Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
|
||||
"""Representation of a Demo Calendar for a current event."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event data."""
|
||||
middle_of_event = dt_util.now() \
|
||||
- dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': middle_of_event.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (middle_of_event + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Current Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendar(CalendarEventDevice):
|
||||
"""Representation of a Demo Calendar element."""
|
||||
|
||||
def __init__(self, hass, calendar_data, data):
|
||||
"""Initialize Google Calendar but without the API calls."""
|
||||
self.data = calendar_data
|
||||
super().__init__(hass, data)
|
||||
"""
|
||||
Demo platform that has two fake binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Demo Calendar platform."""
|
||||
calendar_data_future = DemoGoogleCalendarDataFuture()
|
||||
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
||||
add_devices([
|
||||
DemoGoogleCalendar(hass, calendar_data_future, {
|
||||
CONF_NAME: 'Future Event',
|
||||
CONF_DEVICE_ID: 'future_event',
|
||||
}),
|
||||
|
||||
DemoGoogleCalendar(hass, calendar_data_current, {
|
||||
CONF_NAME: 'Current Event',
|
||||
CONF_DEVICE_ID: 'current_event',
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
class DemoGoogleCalendarData(object):
|
||||
"""Representation of a Demo Calendar element."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def update(self):
|
||||
"""Return true so entity knows we have new data."""
|
||||
return True
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
||||
"""Representation of a Demo Calendar for a future event."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event to a future event."""
|
||||
one_hour_from_now = dt_util.now() \
|
||||
+ dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': one_hour_from_now.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (one_hour_from_now + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Future Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
|
||||
"""Representation of a Demo Calendar for a current event."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event data."""
|
||||
middle_of_event = dt_util.now() \
|
||||
- dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': middle_of_event.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (middle_of_event + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Current Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendar(CalendarEventDevice):
|
||||
"""Representation of a Demo Calendar element."""
|
||||
|
||||
def __init__(self, hass, calendar_data, data):
|
||||
"""Initialize Google Calendar but without the API calls."""
|
||||
self.data = calendar_data
|
||||
super().__init__(hass, data)
|
||||
|
||||
@@ -1,78 +1,77 @@
|
||||
"""
|
||||
Support for Google Calendar Search binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.google_calendar/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import (
|
||||
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
|
||||
GoogleCalendarService)
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
||||
'orderBy': 'startTime',
|
||||
'maxResults': 1,
|
||||
'singleEvents': True,
|
||||
}
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
"""Set up the calendar platform for event devices."""
|
||||
if disc_info is None:
|
||||
return
|
||||
|
||||
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
||||
return
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
|
||||
disc_info[CONF_CAL_ID], data)
|
||||
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
"""A calendar event device."""
|
||||
|
||||
def __init__(self, hass, calendar_service, calendar, data):
|
||||
"""Create the Calendar event device."""
|
||||
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||
data.get('search', None))
|
||||
super().__init__(hass, data)
|
||||
|
||||
|
||||
class GoogleCalendarData(object):
|
||||
"""Class to utilize calendar service object to get next event."""
|
||||
|
||||
def __init__(self, calendar_service, calendar_id, search=None):
|
||||
"""Set up how we are going to search the google calendar."""
|
||||
self.calendar_service = calendar_service
|
||||
self.calendar_id = calendar_id
|
||||
self.search = search
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
if self.search:
|
||||
params['q'] = self.search
|
||||
|
||||
events = service.events() # pylint: disable=no-member
|
||||
result = events.list(**params).execute()
|
||||
|
||||
items = result.get('items', [])
|
||||
self.event = items[0] if len(items) == 1 else None
|
||||
return True
|
||||
"""
|
||||
Support for Google Calendar Search binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.google_calendar/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import (
|
||||
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
|
||||
GoogleCalendarService)
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
||||
'orderBy': 'startTime',
|
||||
'maxResults': 1,
|
||||
'singleEvents': True,
|
||||
}
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
"""Set up the calendar platform for event devices."""
|
||||
if disc_info is None:
|
||||
return
|
||||
|
||||
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
||||
return
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
|
||||
disc_info[CONF_CAL_ID], data)
|
||||
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
||||
|
||||
|
||||
class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
"""A calendar event device."""
|
||||
|
||||
def __init__(self, hass, calendar_service, calendar, data):
|
||||
"""Create the Calendar event device."""
|
||||
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||
data.get('search', None))
|
||||
super().__init__(hass, data)
|
||||
|
||||
|
||||
class GoogleCalendarData(object):
|
||||
"""Class to utilize calendar service object to get next event."""
|
||||
|
||||
def __init__(self, calendar_service, calendar_id, search=None):
|
||||
"""Set up how we are going to search the google calendar."""
|
||||
self.calendar_service = calendar_service
|
||||
self.calendar_id = calendar_id
|
||||
self.search = search
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.now().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
if self.search:
|
||||
params['q'] = self.search
|
||||
|
||||
events = service.events() # pylint: disable=no-member
|
||||
result = events.list(**params).execute()
|
||||
|
||||
items = result.get('items', [])
|
||||
self.event = items[0] if len(items) == 1 else None
|
||||
return True
|
||||
|
||||
@@ -138,7 +138,7 @@ class Camera(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(None, self.camera_image)
|
||||
return self.hass.async_add_job(self.camera_image)
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
@@ -241,7 +241,7 @@ class CameraView(HomeAssistantView):
|
||||
return web.Response(status=status)
|
||||
|
||||
authenticated = (request[KEY_AUTHENTICATED] or
|
||||
request.GET.get('token') in camera.access_tokens)
|
||||
request.query.get('token') in camera.access_tokens)
|
||||
|
||||
if not authenticated:
|
||||
return web.Response(status=401)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
This component provides basic support for Netgear Arlo IP cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND
|
||||
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS):
|
||||
cv.string,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up an Arlo IP Camera."""
|
||||
arlo = hass.data.get('arlo')
|
||||
if not arlo:
|
||||
return False
|
||||
|
||||
cameras = []
|
||||
for camera in arlo.cameras:
|
||||
cameras.append(ArloCam(hass, camera, config))
|
||||
|
||||
async_add_devices(cameras, True)
|
||||
return True
|
||||
|
||||
|
||||
class ArloCam(Camera):
|
||||
"""An implementation of a Netgear Arlo IP camera."""
|
||||
|
||||
def __init__(self, hass, camera, device_info):
|
||||
"""Initialize an Arlo camera."""
|
||||
super().__init__()
|
||||
|
||||
self._camera = camera
|
||||
self._name = self._camera.name
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
return self._camera.last_image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
video = self._camera.last_video
|
||||
if not video:
|
||||
return
|
||||
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
video.video_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Camera model."""
|
||||
return self._camera.model_id
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
@@ -103,8 +103,8 @@ class GenericCamera(Camera):
|
||||
_LOGGER.error("Error getting camera image: %s", error)
|
||||
return self._last_image
|
||||
|
||||
self._last_image = yield from self.hass.loop.run_in_executor(
|
||||
None, fetch)
|
||||
self._last_image = yield from self.hass.async_add_job(
|
||||
fetch)
|
||||
# async
|
||||
else:
|
||||
try:
|
||||
|
||||
@@ -88,8 +88,8 @@ class MjpegCamera(Camera):
|
||||
# DigestAuth is not supported
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION or \
|
||||
self._still_image_url is None:
|
||||
image = yield from self.hass.loop.run_in_executor(
|
||||
None, self.camera_image)
|
||||
image = yield from self.hass.async_add_job(
|
||||
self.camera_image)
|
||||
return image
|
||||
|
||||
websession = async_get_clientsession(self.hass)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Support for ONVIF Cameras with FFmpeg as decoder.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.onvif/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import (
|
||||
DATA_FFMPEG)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['onvif-py3==0.1.3',
|
||||
'suds-py3==1.3.3.0',
|
||||
'http://github.com/tgaugry/suds-passworddigest-py3'
|
||||
'/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip'
|
||||
'#suds-passworddigest-py3==0.1.2a']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
DEFAULT_NAME = 'ONVIF Camera'
|
||||
DEFAULT_PORT = 5000
|
||||
DEFAULT_USERNAME = 'admin'
|
||||
DEFAULT_PASSWORD = '888888'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up a ONVIF camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)):
|
||||
return
|
||||
async_add_devices([ONVIFCamera(hass, config)])
|
||||
|
||||
|
||||
class ONVIFCamera(Camera):
|
||||
"""An implementation of an ONVIF camera."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a ONVIF camera."""
|
||||
from onvif import ONVIFService
|
||||
super().__init__()
|
||||
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._ffmpeg_arguments = '-q:v 2'
|
||||
media = ONVIFService(
|
||||
'http://{}:{}/onvif/device_service'.format(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT)),
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
'{}/deps/onvif/wsdl/media.wsdl'.format(hass.config.config_dir)
|
||||
)
|
||||
self._input = media.GetStreamUri().Uri
|
||||
_LOGGER.debug("ONVIF Camera Using the following URL for %s: %s",
|
||||
self._name, self._input)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
ffmpeg = ImageFrame(
|
||||
self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
|
||||
|
||||
image = yield from ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._ffmpeg_arguments)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary,
|
||||
loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
self._input, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
@@ -1,250 +1,250 @@
|
||||
"""
|
||||
Support for Synology Surveillance Station Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.synology/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_create_clientsession,
|
||||
async_aiohttp_proxy_web)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
DEFAULT_TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
|
||||
QUERY_CGI = 'query.cgi'
|
||||
QUERY_API = 'SYNO.API.Info'
|
||||
AUTH_API = 'SYNO.API.Auth'
|
||||
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
|
||||
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
|
||||
SESSION_ID = '0'
|
||||
|
||||
WEBAPI_PATH = '/webapi/'
|
||||
AUTH_PATH = 'auth.cgi'
|
||||
CAMERA_PATH = 'camera.cgi'
|
||||
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
SYNO_API_URL = '{0}{1}{2}'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up 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
|
||||
syno_api_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
||||
|
||||
query_payload = {
|
||||
'api': QUERY_API,
|
||||
'method': 'Query',
|
||||
'version': '1',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
)
|
||||
|
||||
# Skip content type check because Synology doesn't return JSON with
|
||||
# right content type
|
||||
query_resp = yield from query_req.json(content_type=None)
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_api_url)
|
||||
return False
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||
|
||||
session_id = yield from get_session_id(
|
||||
hass,
|
||||
websession_init,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url,
|
||||
timeout
|
||||
)
|
||||
|
||||
# init websession
|
||||
websession = async_create_clientsession(
|
||||
hass, verify_ssl, cookies={'id': session_id})
|
||||
|
||||
# Use SessionID to get cameras in system
|
||||
syno_camera_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
||||
|
||||
camera_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'List',
|
||||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
camera_req = yield from websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_camera_url)
|
||||
return False
|
||||
|
||||
camera_resp = yield from camera_req.json(content_type=None)
|
||||
cameras = camera_resp['data']['cameras']
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
for camera in cameras:
|
||||
if not config.get(CONF_WHITELIST):
|
||||
camera_id = camera['id']
|
||||
snapshot_path = camera['snapshot_path']
|
||||
|
||||
device = SynologyCamera(
|
||||
hass, websession, config, camera_id, camera['name'],
|
||||
snapshot_path, streaming_path, camera_path, auth_path, timeout
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||
"""Get a session id."""
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': username,
|
||||
'passwd': password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
)
|
||||
auth_resp = yield from auth_req.json(content_type=None)
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", login_url)
|
||||
return False
|
||||
|
||||
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
||||
def __init__(self, hass, websession, config, camera_id,
|
||||
camera_name, snapshot_path, streaming_path, camera_path,
|
||||
auth_path, timeout):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._websession = websession
|
||||
self._name = camera_name
|
||||
self._synology_url = config.get(CONF_URL)
|
||||
self._camera_name = config.get(CONF_CAMERA_NAME)
|
||||
self._stream_id = config.get(CONF_STREAM_ID)
|
||||
self._camera_id = camera_id
|
||||
self._snapshot_path = snapshot_path
|
||||
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."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
image_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._camera_path)
|
||||
|
||||
image_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'GetSnapshot',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||
response = yield from self._websession.get(
|
||||
image_url,
|
||||
params=image_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Error fetching %s", image_url)
|
||||
return None
|
||||
|
||||
image = yield from response.read()
|
||||
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
streaming_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
||||
|
||||
streaming_payload = {
|
||||
'api': STREAMING_API,
|
||||
'method': 'Stream',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'
|
||||
}
|
||||
stream_coro = self._websession.get(
|
||||
streaming_url, params=streaming_payload)
|
||||
|
||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._name
|
||||
"""
|
||||
Support for Synology Surveillance Station Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.synology/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_create_clientsession,
|
||||
async_aiohttp_proxy_web)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
DEFAULT_TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
|
||||
QUERY_CGI = 'query.cgi'
|
||||
QUERY_API = 'SYNO.API.Info'
|
||||
AUTH_API = 'SYNO.API.Auth'
|
||||
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
|
||||
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
|
||||
SESSION_ID = '0'
|
||||
|
||||
WEBAPI_PATH = '/webapi/'
|
||||
AUTH_PATH = 'auth.cgi'
|
||||
CAMERA_PATH = 'camera.cgi'
|
||||
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
SYNO_API_URL = '{0}{1}{2}'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up 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
|
||||
syno_api_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
||||
|
||||
query_payload = {
|
||||
'api': QUERY_API,
|
||||
'method': 'Query',
|
||||
'version': '1',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
)
|
||||
|
||||
# Skip content type check because Synology doesn't return JSON with
|
||||
# right content type
|
||||
query_resp = yield from query_req.json(content_type=None)
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_api_url)
|
||||
return False
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||
|
||||
session_id = yield from get_session_id(
|
||||
hass,
|
||||
websession_init,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url,
|
||||
timeout
|
||||
)
|
||||
|
||||
# init websession
|
||||
websession = async_create_clientsession(
|
||||
hass, verify_ssl, cookies={'id': session_id})
|
||||
|
||||
# Use SessionID to get cameras in system
|
||||
syno_camera_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
||||
|
||||
camera_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'List',
|
||||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
camera_req = yield from websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_camera_url)
|
||||
return False
|
||||
|
||||
camera_resp = yield from camera_req.json(content_type=None)
|
||||
cameras = camera_resp['data']['cameras']
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
for camera in cameras:
|
||||
if not config.get(CONF_WHITELIST):
|
||||
camera_id = camera['id']
|
||||
snapshot_path = camera['snapshot_path']
|
||||
|
||||
device = SynologyCamera(
|
||||
hass, websession, config, camera_id, camera['name'],
|
||||
snapshot_path, streaming_path, camera_path, auth_path, timeout
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||
"""Get a session id."""
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': username,
|
||||
'passwd': password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
)
|
||||
auth_resp = yield from auth_req.json(content_type=None)
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.exception("Error on %s", login_url)
|
||||
return False
|
||||
|
||||
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
||||
def __init__(self, hass, websession, config, camera_id,
|
||||
camera_name, snapshot_path, streaming_path, camera_path,
|
||||
auth_path, timeout):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
self._websession = websession
|
||||
self._name = camera_name
|
||||
self._synology_url = config.get(CONF_URL)
|
||||
self._camera_name = config.get(CONF_CAMERA_NAME)
|
||||
self._stream_id = config.get(CONF_STREAM_ID)
|
||||
self._camera_id = camera_id
|
||||
self._snapshot_path = snapshot_path
|
||||
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."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
image_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._camera_path)
|
||||
|
||||
image_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'GetSnapshot',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||
response = yield from self._websession.get(
|
||||
image_url,
|
||||
params=image_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Error fetching %s", image_url)
|
||||
return None
|
||||
|
||||
image = yield from response.read()
|
||||
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
streaming_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
||||
|
||||
streaming_payload = {
|
||||
'api': STREAMING_API,
|
||||
'method': 'Stream',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'
|
||||
}
|
||||
stream_coro = self._websession.get(
|
||||
streaming_url, params=streaming_payload)
|
||||
|
||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._name
|
||||
|
||||
@@ -213,8 +213,8 @@ def async_setup(hass, config):
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
yield from component.async_setup(config)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -569,8 +569,8 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.set_temperature, **kwargs))
|
||||
return self.hass.async_add_job(
|
||||
ft.partial(self.set_temperature, **kwargs))
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set new target humidity."""
|
||||
@@ -581,8 +581,7 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_humidity, humidity)
|
||||
return self.hass.async_add_job(self.set_humidity, humidity)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
@@ -593,8 +592,7 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_fan_mode, fan)
|
||||
return self.hass.async_add_job(self.set_fan_mode, fan)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
@@ -605,8 +603,7 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_operation_mode, operation_mode)
|
||||
return self.hass.async_add_job(self.set_operation_mode, operation_mode)
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
@@ -617,8 +614,7 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_swing_mode, swing_mode)
|
||||
return self.hass.async_add_job(self.set_swing_mode, swing_mode)
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
@@ -629,8 +625,7 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_away_mode_on)
|
||||
return self.hass.async_add_job(self.turn_away_mode_on)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
@@ -641,8 +636,7 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_away_mode_off)
|
||||
return self.hass.async_add_job(self.turn_away_mode_off)
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Set new target hold mode."""
|
||||
@@ -653,8 +647,7 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_hold_mode, hold_mode)
|
||||
return self.hass.async_add_job(self.set_hold_mode, hold_mode)
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
@@ -665,8 +658,7 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_aux_heat_on)
|
||||
return self.hass.async_add_job(self.turn_aux_heat_on)
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
@@ -677,8 +669,7 @@ class ClimateDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_aux_heat_off)
|
||||
return self.hass.async_add_job(self.turn_aux_heat_off)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
Platform for Flexit AC units with CI66 Modbus adapter.
|
||||
|
||||
Example configuration:
|
||||
|
||||
climate:
|
||||
- platform: flexit
|
||||
name: Main AC
|
||||
slave: 21
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/climate.flexit/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_SLAVE, TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
import homeassistant.components.modbus as modbus
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyflexit==0.3']
|
||||
DEPENDENCIES = ['modbus']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SLAVE): vol.All(int, vol.Range(min=0, max=32)),
|
||||
vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Flexit Platform."""
|
||||
modbus_slave = config.get(CONF_SLAVE, None)
|
||||
name = config.get(CONF_NAME, None)
|
||||
add_devices([Flexit(modbus_slave, name)], True)
|
||||
|
||||
|
||||
class Flexit(ClimateDevice):
|
||||
"""Representation of a Flexit AC unit."""
|
||||
|
||||
def __init__(self, modbus_slave, name):
|
||||
"""Initialize the unit."""
|
||||
from pyflexit import pyflexit
|
||||
self._name = name
|
||||
self._slave = modbus_slave
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_fan_mode = None
|
||||
self._current_operation = None
|
||||
self._fan_list = ['Off', 'Low', 'Medium', 'High']
|
||||
self._current_operation = None
|
||||
self._filter_hours = None
|
||||
self._filter_alarm = None
|
||||
self._heat_recovery = None
|
||||
self._heater_enabled = False
|
||||
self._heating = None
|
||||
self._cooling = None
|
||||
self._alarm = False
|
||||
self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave)
|
||||
|
||||
def update(self):
|
||||
"""Update unit attributes."""
|
||||
if not self.unit.update():
|
||||
_LOGGER.warning("Modbus read failed")
|
||||
|
||||
self._target_temperature = self.unit.get_target_temp
|
||||
self._current_temperature = self.unit.get_temp
|
||||
self._current_fan_mode =\
|
||||
self._fan_list[self.unit.get_fan_speed]
|
||||
self._filter_hours = self.unit.get_filter_hours
|
||||
# Mechanical heat recovery, 0-100%
|
||||
self._heat_recovery = self.unit.get_heat_recovery
|
||||
# Heater active 0-100%
|
||||
self._heating = self.unit.get_heating
|
||||
# Cooling active 0-100%
|
||||
self._cooling = self.unit.get_cooling
|
||||
# Filter alarm 0/1
|
||||
self._filter_alarm = self.unit.get_filter_alarm
|
||||
# Heater enabled or not. Does not mean it's necessarily heating
|
||||
self._heater_enabled = self.unit.get_heater_enabled
|
||||
# Current operation mode
|
||||
self._current_operation = self.unit.get_operation
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
return {
|
||||
'filter_hours': self._filter_hours,
|
||||
'filter_alarm': self._filter_alarm,
|
||||
'heat_recovery': self._heat_recovery,
|
||||
'heating': self._heating,
|
||||
'heater_enabled': self._heater_enabled,
|
||||
'cooling': self._cooling
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the climate device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.unit.set_temp(self._target_temperature)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new fan mode."""
|
||||
self.unit.set_fan_speed(fan)
|
||||
@@ -1,4 +1,4 @@
|
||||
"""
|
||||
"""
|
||||
Tado component to create a climate device for each zone.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
|
||||
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([WinkAC(climate, hass, temp_unit)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method,too-many-public-methods, too-many-branches
|
||||
# pylint: disable=abstract-method
|
||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink thermostat."""
|
||||
|
||||
|
||||
@@ -14,11 +14,13 @@ from homeassistant import core
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import script
|
||||
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.15.0']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
ATTR_SENTENCE = 'sentence'
|
||||
DOMAIN = 'conversation'
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
@@ -29,9 +31,12 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
|
||||
cv.string: vol.Schema({
|
||||
vol.Required(ATTR_SENTENCE): cv.string,
|
||||
vol.Required('action'): cv.SCRIPT_SCHEMA,
|
||||
})
|
||||
})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@@ -40,9 +45,30 @@ def setup(hass, config):
|
||||
from fuzzywuzzy import process as fuzzyExtract
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
config = config.get(DOMAIN, {})
|
||||
|
||||
choices = {attrs[ATTR_SENTENCE]: script.Script(
|
||||
hass,
|
||||
attrs['action'],
|
||||
name)
|
||||
for name, attrs in config.items()}
|
||||
|
||||
def process(service):
|
||||
"""Parse text into commands."""
|
||||
# if actually configured
|
||||
if choices:
|
||||
text = service.data[ATTR_TEXT]
|
||||
match = fuzzyExtract.extractOne(text, choices.keys())
|
||||
scorelimit = 60 # arbitrary value
|
||||
logging.info(
|
||||
'matched up text %s and found %s',
|
||||
text,
|
||||
[match[0] if match[1] > scorelimit else 'nothing']
|
||||
)
|
||||
if match[1] > scorelimit:
|
||||
choices[match[0]].run() # run respective script
|
||||
return
|
||||
|
||||
text = service.data[ATTR_TEXT]
|
||||
match = REGEX_TURN_COMMAND.match(text)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ from homeassistant.const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'cover'
|
||||
DEPENDENCIES = ['group']
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
GROUP_NAME_ALL_COVERS = 'all covers'
|
||||
@@ -175,8 +176,8 @@ def async_setup(hass, config):
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service_name in SERVICE_TO_METHOD:
|
||||
@@ -263,8 +264,7 @@ class CoverDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.open_cover, **kwargs))
|
||||
return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs))
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close cover."""
|
||||
@@ -275,8 +275,7 @@ class CoverDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.close_cover, **kwargs))
|
||||
return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs))
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
@@ -287,8 +286,8 @@ class CoverDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.set_cover_position, **kwargs))
|
||||
return self.hass.async_add_job(
|
||||
ft.partial(self.set_cover_position, **kwargs))
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
@@ -299,8 +298,7 @@ class CoverDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.stop_cover, **kwargs))
|
||||
return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs))
|
||||
|
||||
def open_cover_tilt(self, **kwargs):
|
||||
"""Open the cover tilt."""
|
||||
@@ -311,8 +309,8 @@ class CoverDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.open_cover_tilt, **kwargs))
|
||||
return self.hass.async_add_job(
|
||||
ft.partial(self.open_cover_tilt, **kwargs))
|
||||
|
||||
def close_cover_tilt(self, **kwargs):
|
||||
"""Close the cover tilt."""
|
||||
@@ -323,8 +321,8 @@ class CoverDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.close_cover_tilt, **kwargs))
|
||||
return self.hass.async_add_job(
|
||||
ft.partial(self.close_cover_tilt, **kwargs))
|
||||
|
||||
def set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
@@ -335,8 +333,8 @@ class CoverDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.set_cover_tilt_position, **kwargs))
|
||||
return self.hass.async_add_job(
|
||||
ft.partial(self.set_cover_tilt_position, **kwargs))
|
||||
|
||||
def stop_cover_tilt(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
@@ -347,5 +345,5 @@ class CoverDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.stop_cover_tilt, **kwargs))
|
||||
return self.hass.async_add_job(
|
||||
ft.partial(self.stop_cover_tilt, **kwargs))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
The homematic cover platform.
|
||||
The HomeMatic cover platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.homematic/
|
||||
@@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class HMCover(HMDevice, CoverDevice):
|
||||
"""Representation a Homematic Cover."""
|
||||
"""Representation a HomeMatic Cover."""
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
@@ -70,7 +70,6 @@ class HMCover(HMDevice, CoverDevice):
|
||||
self._hmdevice.stop(self._channel)
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from hm metadata."""
|
||||
# Add state to data dict
|
||||
"""Generate a data dictoinary (self._data) from metadata."""
|
||||
self._state = "LEVEL"
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
|
||||
@@ -14,7 +14,9 @@ import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, ATTR_TILT_POSITION, SUPPORT_OPEN_TILT,
|
||||
SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION,
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION)
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION,
|
||||
ATTR_POSITION)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
|
||||
STATE_CLOSED, STATE_UNKNOWN)
|
||||
@@ -29,6 +31,8 @@ DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_TILT_COMMAND_TOPIC = 'tilt_command_topic'
|
||||
CONF_TILT_STATUS_TOPIC = 'tilt_status_topic'
|
||||
CONF_POSITION_TOPIC = 'set_position_topic'
|
||||
CONF_SET_POSITION_TEMPLATE = 'set_position_template'
|
||||
|
||||
CONF_PAYLOAD_OPEN = 'payload_open'
|
||||
CONF_PAYLOAD_CLOSE = 'payload_close'
|
||||
@@ -55,10 +59,17 @@ DEFAULT_TILT_MAX = 100
|
||||
DEFAULT_TILT_OPTIMISTIC = False
|
||||
DEFAULT_TILT_INVERT_STATE = False
|
||||
|
||||
OPEN_CLOSE_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP)
|
||||
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_COMMAND_TOPIC, default=None): valid_publish_topic,
|
||||
vol.Optional(CONF_POSITION_TOPIC, default=None): valid_publish_topic,
|
||||
vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
|
||||
@@ -87,6 +98,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
set_position_template = config.get(CONF_SET_POSITION_TEMPLATE)
|
||||
if set_position_template is not None:
|
||||
set_position_template.hass = hass
|
||||
|
||||
async_add_devices([MqttCover(
|
||||
config.get(CONF_NAME),
|
||||
@@ -109,6 +123,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
config.get(CONF_TILT_MAX),
|
||||
config.get(CONF_TILT_STATE_OPTIMISTIC),
|
||||
config.get(CONF_TILT_INVERT_STATE),
|
||||
config.get(CONF_POSITION_TOPIC),
|
||||
set_position_template,
|
||||
)])
|
||||
|
||||
|
||||
@@ -120,7 +136,7 @@ class MqttCover(CoverDevice):
|
||||
payload_open, payload_close, payload_stop,
|
||||
optimistic, value_template, tilt_open_position,
|
||||
tilt_closed_position, tilt_min, tilt_max, tilt_optimistic,
|
||||
tilt_invert):
|
||||
tilt_invert, position_topic, set_position_template):
|
||||
"""Initialize the cover."""
|
||||
self._position = None
|
||||
self._state = None
|
||||
@@ -145,6 +161,8 @@ class MqttCover(CoverDevice):
|
||||
self._tilt_max = tilt_max
|
||||
self._tilt_optimistic = tilt_optimistic
|
||||
self._tilt_invert = tilt_invert
|
||||
self._position_topic = position_topic
|
||||
self._set_position_template = set_position_template
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
@@ -233,9 +251,11 @@ class MqttCover(CoverDevice):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
supported_features = 0
|
||||
if self._command_topic is not None:
|
||||
supported_features = OPEN_CLOSE_FEATURES
|
||||
|
||||
if self.current_cover_position is not None:
|
||||
if self._position_topic is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
if self._tilt_command_topic is not None:
|
||||
@@ -315,6 +335,22 @@ class MqttCover(CoverDevice):
|
||||
mqtt.async_publish(self.hass, self._tilt_command_topic,
|
||||
level, self._qos, self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
if self._set_position_template is not None:
|
||||
try:
|
||||
position = self._set_position_template.async_render(
|
||||
**kwargs)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._state = None
|
||||
|
||||
mqtt.async_publish(self.hass, self._position_topic,
|
||||
position, self._qos, self._retain)
|
||||
|
||||
def find_percentage_in_range(self, position):
|
||||
"""Find the 0-100% value within the specified range."""
|
||||
# the range of motion as defined by the min max values
|
||||
|
||||
@@ -12,10 +12,16 @@ from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
|
||||
'#pymyq==0.0.8']
|
||||
REQUIREMENTS = ['pymyq==0.0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'myq'
|
||||
|
||||
NOTIFICATION_ID = 'myq_notification'
|
||||
NOTIFICATION_TITLE = 'MyQ Cover Setup'
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): cv.string,
|
||||
@@ -23,8 +29,6 @@ COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
DEFAULT_NAME = 'myq'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the MyQ component."""
|
||||
@@ -33,23 +37,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
brand = config.get(CONF_TYPE)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
myq = pymyq(username, password, brand)
|
||||
|
||||
if not myq.is_supported_brand():
|
||||
logger.error("Unsupported type. See documentation")
|
||||
return
|
||||
|
||||
if not myq.is_login_valid():
|
||||
logger.error("Username or Password is incorrect")
|
||||
return
|
||||
|
||||
try:
|
||||
if not myq.is_supported_brand():
|
||||
raise ValueError("Unsupported type. See documentation")
|
||||
|
||||
if not myq.is_login_valid():
|
||||
raise ValueError("Username or Password is incorrect")
|
||||
|
||||
add_devices(MyQDevice(myq, door) for door in myq.get_garage_doors())
|
||||
except (TypeError, KeyError, NameError) as ex:
|
||||
logger.error("%s", ex)
|
||||
return True
|
||||
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
|
||||
class MyQDevice(CoverDevice):
|
||||
|
||||
@@ -41,7 +41,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Initialize the Z-Wave rollershutter."""
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._network = hass.data[zwave.ZWAVE_NETWORK]
|
||||
self._network = hass.data[zwave.const.DATA_NETWORK]
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
self._current_position = None
|
||||
|
||||
@@ -27,6 +27,7 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -35,12 +36,13 @@ from homeassistant.util.yaml import dump
|
||||
from homeassistant.helpers.event import async_track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID)
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID,
|
||||
CONF_ICON, ATTR_ICON)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'device_tracker'
|
||||
DEPENDENCIES = ['zone']
|
||||
DEPENDENCIES = ['zone', 'group']
|
||||
|
||||
GROUP_NAME_ALL_DEVICES = 'all devices'
|
||||
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
||||
@@ -121,15 +123,10 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Set up the device tracker."""
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
|
||||
try:
|
||||
conf = config.get(DOMAIN, [])
|
||||
except vol.Invalid as ex:
|
||||
async_log_exception(ex, DOMAIN, config, hass)
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if conf else {}
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
conf = config.get(DOMAIN, [])
|
||||
conf = conf[0] if conf else {}
|
||||
consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
|
||||
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
||||
@@ -150,14 +147,14 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
scanner = yield from platform.async_get_scanner(
|
||||
hass, {DOMAIN: p_config})
|
||||
elif hasattr(platform, 'get_scanner'):
|
||||
scanner = yield from hass.loop.run_in_executor(
|
||||
None, platform.get_scanner, hass, {DOMAIN: p_config})
|
||||
scanner = yield from hass.async_add_job(
|
||||
platform.get_scanner, hass, {DOMAIN: p_config})
|
||||
elif hasattr(platform, 'async_setup_scanner'):
|
||||
setup = yield from platform.async_setup_scanner(
|
||||
hass, p_config, tracker.async_see, disc_info)
|
||||
elif hasattr(platform, 'setup_scanner'):
|
||||
setup = yield from hass.loop.run_in_executor(
|
||||
None, platform.setup_scanner, hass, p_config, tracker.see,
|
||||
setup = yield from hass.async_add_job(
|
||||
platform.setup_scanner, hass, p_config, tracker.see,
|
||||
disc_info)
|
||||
else:
|
||||
raise HomeAssistantError("Invalid device_tracker platform.")
|
||||
@@ -179,7 +176,7 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
if setup_tasks:
|
||||
yield from asyncio.wait(setup_tasks, loop=hass.loop)
|
||||
|
||||
yield from tracker.async_setup_group()
|
||||
tracker.async_setup_group()
|
||||
|
||||
@callback
|
||||
def async_device_tracker_discovered(service, info):
|
||||
@@ -209,8 +206,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
|
||||
yield from tracker.async_see(**args)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
hass.services.async_register(
|
||||
@@ -232,7 +229,7 @@ class DeviceTracker(object):
|
||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new
|
||||
self.group = None # type: group.Group
|
||||
self.group = None
|
||||
self._is_updating = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
for dev in devices:
|
||||
@@ -245,18 +242,21 @@ class DeviceTracker(object):
|
||||
def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
|
||||
battery: str=None, attributes: dict=None,
|
||||
source_type: str=SOURCE_TYPE_GPS):
|
||||
source_type: str=SOURCE_TYPE_GPS, picture: str=None,
|
||||
icon: str=None):
|
||||
"""Notify the device tracker that you see a device."""
|
||||
self.hass.add_job(
|
||||
self.async_see(mac, dev_id, host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes, source_type)
|
||||
gps_accuracy, battery, attributes, source_type,
|
||||
picture, icon)
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||
location_name: str=None, gps: GPSType=None,
|
||||
gps_accuracy=None, battery: str=None, attributes: dict=None,
|
||||
source_type: str=SOURCE_TYPE_GPS):
|
||||
source_type: str=SOURCE_TYPE_GPS, picture: str=None,
|
||||
icon: str=None):
|
||||
"""Notify the device tracker that you see a device.
|
||||
|
||||
This method is a coroutine.
|
||||
@@ -284,7 +284,8 @@ class DeviceTracker(object):
|
||||
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '),
|
||||
picture=picture, icon=icon)
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
@@ -302,9 +303,10 @@ class DeviceTracker(object):
|
||||
})
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group is not None:
|
||||
yield from self.group.async_update_tracked_entity_ids(
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
if self.group and self.track_new:
|
||||
self.group.async_set_group(
|
||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
|
||||
|
||||
# lookup mac vendor string to be stored in config
|
||||
yield from device.set_vendor_for_mac()
|
||||
@@ -322,20 +324,23 @@ class DeviceTracker(object):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
with (yield from self._is_updating):
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, update_config, self.hass.config.path(YAML_DEVICES),
|
||||
yield from self.hass.async_add_job(
|
||||
update_config, self.hass.config.path(YAML_DEVICES),
|
||||
dev_id, device)
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def async_setup_group(self):
|
||||
"""Initialize group for all tracked devices.
|
||||
|
||||
This method is a coroutine.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||
if dev.track)
|
||||
self.group = yield from group.Group.async_create_group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
entity_ids = [dev.entity_id for dev in self.devices.values()
|
||||
if dev.track]
|
||||
|
||||
self.group = get_component('group')
|
||||
self.group.async_set_group(
|
||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||
name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids)
|
||||
|
||||
@callback
|
||||
def async_update_stale(self, now: dt_util.dt.datetime):
|
||||
@@ -381,6 +386,7 @@ class Device(Entity):
|
||||
battery = None # type: str
|
||||
attributes = None # type: dict
|
||||
vendor = None # type: str
|
||||
icon = None # type: str
|
||||
|
||||
# Track if the last update of this device was HOME.
|
||||
last_update_home = False
|
||||
@@ -388,7 +394,7 @@ class Device(Entity):
|
||||
|
||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track: bool, dev_id: str, mac: str, name: str=None,
|
||||
picture: str=None, gravatar: str=None,
|
||||
picture: str=None, gravatar: str=None, icon: str=None,
|
||||
hide_if_away: bool=False, vendor: str=None) -> None:
|
||||
"""Initialize a device."""
|
||||
self.hass = hass
|
||||
@@ -414,6 +420,8 @@ class Device(Entity):
|
||||
else:
|
||||
self.config_picture = picture
|
||||
|
||||
self.icon = icon
|
||||
|
||||
self.away_hide = hide_if_away
|
||||
self.vendor = vendor
|
||||
|
||||
@@ -608,7 +616,7 @@ class DeviceScanner(object):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(None, self.scan_devices)
|
||||
return self.hass.async_add_job(self.scan_devices)
|
||||
|
||||
def get_device_name(self, mac: str) -> str:
|
||||
"""Get device name from mac."""
|
||||
@@ -619,7 +627,7 @@ class DeviceScanner(object):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(None, self.get_device_name, mac)
|
||||
return self.hass.async_add_job(self.get_device_name, mac)
|
||||
|
||||
|
||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
@@ -637,6 +645,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
||||
"""
|
||||
dev_schema = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON, default=False):
|
||||
vol.Any(None, cv.icon),
|
||||
vol.Optional('track', default=False): cv.boolean,
|
||||
vol.Optional(CONF_MAC, default=None):
|
||||
vol.Any(None, vol.All(cv.string, vol.Upper)),
|
||||
@@ -650,8 +660,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
||||
try:
|
||||
result = []
|
||||
try:
|
||||
devices = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, path)
|
||||
devices = yield from hass.async_add_job(
|
||||
load_yaml_config_file, path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Unable to load %s: %s", path, str(err))
|
||||
return []
|
||||
@@ -728,6 +738,7 @@ def update_config(path: str, dev_id: str, device: Device):
|
||||
device = {device.dev_id: {
|
||||
ATTR_NAME: device.name,
|
||||
ATTR_MAC: device.mac,
|
||||
ATTR_ICON: device.icon,
|
||||
'picture': device.config_picture,
|
||||
'track': device.track,
|
||||
CONF_AWAY_HIDE: device.away_hide,
|
||||
|
||||
@@ -118,25 +118,29 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
self.protocol = config[CONF_PROTOCOL]
|
||||
self.mode = config[CONF_MODE]
|
||||
self.port = config[CONF_PORT]
|
||||
self.ssh_args = {}
|
||||
|
||||
if self.protocol == 'ssh':
|
||||
|
||||
self.ssh_args['port'] = self.port
|
||||
if self.ssh_key:
|
||||
self.ssh_args['ssh_key'] = self.ssh_key
|
||||
elif self.password:
|
||||
self.ssh_args['password'] = self.password
|
||||
else:
|
||||
if not (self.ssh_key or self.password):
|
||||
_LOGGER.error("No password or private key specified")
|
||||
self.success_init = False
|
||||
return
|
||||
|
||||
self.connection = SshConnection(self.host, self.port,
|
||||
self.username,
|
||||
self.password,
|
||||
self.ssh_key,
|
||||
self.mode == "ap")
|
||||
else:
|
||||
if not self.password:
|
||||
_LOGGER.error("No password specified")
|
||||
self.success_init = False
|
||||
return
|
||||
|
||||
self.connection = TelnetConnection(self.host, self.port,
|
||||
self.username,
|
||||
self.password,
|
||||
self.mode == "ap")
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
@@ -182,105 +186,9 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
def ssh_connection(self):
|
||||
"""Retrieve data from ASUSWRT via the ssh protocol."""
|
||||
from pexpect import pxssh, exceptions
|
||||
|
||||
ssh = pxssh.pxssh()
|
||||
try:
|
||||
ssh.login(self.host, self.username, **self.ssh_args)
|
||||
except exceptions.EOF as err:
|
||||
_LOGGER.error("Connection refused. SSH enabled?")
|
||||
return None
|
||||
except pxssh.ExceptionPxssh as err:
|
||||
_LOGGER.error("Unable to connect via SSH: %s", str(err))
|
||||
return None
|
||||
|
||||
try:
|
||||
ssh.sendline(_IP_NEIGH_CMD)
|
||||
ssh.prompt()
|
||||
neighbors = ssh.before.split(b'\n')[1:-1]
|
||||
if self.mode == 'ap':
|
||||
ssh.sendline(_ARP_CMD)
|
||||
ssh.prompt()
|
||||
arp_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.sendline(_WL_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.sendline(_NVRAM_CMD)
|
||||
ssh.prompt()
|
||||
nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:]
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
ssh.sendline(_LEASES_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.logout()
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except pxssh.ExceptionPxssh as exc:
|
||||
_LOGGER.error("Unexpected response from router: %s", exc)
|
||||
return None
|
||||
|
||||
def telnet_connection(self):
|
||||
"""Retrieve data from ASUSWRT via the telnet protocol."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'login: ')
|
||||
telnet.write((self.username + '\n').encode('ascii'))
|
||||
telnet.read_until(b'Password: ')
|
||||
telnet.write((self.password + '\n').encode('ascii'))
|
||||
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
|
||||
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
|
||||
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
|
||||
if self.mode == 'ap':
|
||||
telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
|
||||
arp_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
|
||||
nvram_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1].split(b'<')[1:])
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except EOFError:
|
||||
_LOGGER.error("Unexpected response from router")
|
||||
return None
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.error("Connection refused by router. Telnet enabled?")
|
||||
return None
|
||||
except socket.gaierror as exc:
|
||||
_LOGGER.error("Socket exception: %s", exc)
|
||||
return None
|
||||
except OSError as exc:
|
||||
_LOGGER.error("OSError: %s", exc)
|
||||
return None
|
||||
|
||||
def get_asuswrt_data(self):
|
||||
"""Retrieve data from ASUSWRT and return parsed result."""
|
||||
if self.protocol == 'ssh':
|
||||
result = self.ssh_connection()
|
||||
elif self.protocol == 'telnet':
|
||||
result = self.telnet_connection()
|
||||
else:
|
||||
# autodetect protocol
|
||||
result = self.ssh_connection()
|
||||
if result:
|
||||
self.protocol = 'ssh'
|
||||
else:
|
||||
result = self.telnet_connection()
|
||||
if result:
|
||||
self.protocol = 'telnet'
|
||||
result = self.connection.get_result()
|
||||
|
||||
if not result:
|
||||
return {}
|
||||
@@ -363,3 +271,193 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
if match.group('ip') in devices:
|
||||
devices[match.group('ip')]['status'] = match.group('status')
|
||||
return devices
|
||||
|
||||
|
||||
class _Connection:
|
||||
def __init__(self):
|
||||
self._connected = False
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Return connection state."""
|
||||
return self._connected
|
||||
|
||||
def connect(self):
|
||||
"""Mark currenct connection state as connected."""
|
||||
self._connected = True
|
||||
|
||||
def disconnect(self):
|
||||
"""Mark current connection state as disconnected."""
|
||||
self._connected = False
|
||||
|
||||
|
||||
class SshConnection(_Connection):
|
||||
"""Maintains an SSH connection to an ASUS-WRT router."""
|
||||
|
||||
def __init__(self, host, port, username, password, ssh_key, ap):
|
||||
"""Initialize the SSH connection properties."""
|
||||
super(SshConnection, self).__init__()
|
||||
|
||||
self._ssh = None
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._ssh_key = ssh_key
|
||||
self._ap = ap
|
||||
|
||||
def get_result(self):
|
||||
"""Retrieve a single AsusWrtResult through an SSH connection.
|
||||
|
||||
Connect to the SSH server if not currently connected, otherwise
|
||||
use the existing connection.
|
||||
"""
|
||||
from pexpect import pxssh, exceptions
|
||||
|
||||
try:
|
||||
if not self.connected:
|
||||
self.connect()
|
||||
self._ssh.sendline(_IP_NEIGH_CMD)
|
||||
self._ssh.prompt()
|
||||
neighbors = self._ssh.before.split(b'\n')[1:-1]
|
||||
if self._ap:
|
||||
self._ssh.sendline(_ARP_CMD)
|
||||
self._ssh.prompt()
|
||||
arp_result = self._ssh.before.split(b'\n')[1:-1]
|
||||
self._ssh.sendline(_WL_CMD)
|
||||
self._ssh.prompt()
|
||||
leases_result = self._ssh.before.split(b'\n')[1:-1]
|
||||
self._ssh.sendline(_NVRAM_CMD)
|
||||
self._ssh.prompt()
|
||||
nvram_result = self._ssh.before.split(b'\n')[1].split(b'<')[1:]
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
self._ssh.sendline(_LEASES_CMD)
|
||||
self._ssh.prompt()
|
||||
leases_result = self._ssh.before.split(b'\n')[1:-1]
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except exceptions.EOF as err:
|
||||
_LOGGER.error("Connection refused. SSH enabled?")
|
||||
self.disconnect()
|
||||
return None
|
||||
except pxssh.ExceptionPxssh as err:
|
||||
_LOGGER.error("Unexpected SSH error: %s", str(err))
|
||||
self.disconnect()
|
||||
return None
|
||||
except AssertionError as err:
|
||||
_LOGGER.error("Connection to router unavailable: %s", str(err))
|
||||
self.disconnect()
|
||||
return None
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the ASUS-WRT SSH server."""
|
||||
from pexpect import pxssh
|
||||
|
||||
self._ssh = pxssh.pxssh()
|
||||
if self._ssh_key:
|
||||
self._ssh.login(self._host, self._username,
|
||||
ssh_key=self._ssh_key, port=self._port)
|
||||
else:
|
||||
self._ssh.login(self._host, self._username,
|
||||
password=self._password, port=self._port)
|
||||
|
||||
super(SshConnection, self).connect()
|
||||
|
||||
def disconnect(self): \
|
||||
# pylint: disable=broad-except
|
||||
"""Disconnect the current SSH connection."""
|
||||
try:
|
||||
self._ssh.logout()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._ssh = None
|
||||
|
||||
super(SshConnection, self).disconnect()
|
||||
|
||||
|
||||
class TelnetConnection(_Connection):
|
||||
"""Maintains a Telnet connection to an ASUS-WRT router."""
|
||||
|
||||
def __init__(self, host, port, username, password, ap):
|
||||
"""Initialize the Telnet connection properties."""
|
||||
super(TelnetConnection, self).__init__()
|
||||
|
||||
self._telnet = None
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._ap = ap
|
||||
self._prompt_string = None
|
||||
|
||||
def get_result(self):
|
||||
"""Retrieve a single AsusWrtResult through a Telnet connection.
|
||||
|
||||
Connect to the Telnet server if not currently connected, otherwise
|
||||
use the existing connection.
|
||||
"""
|
||||
try:
|
||||
if not self.connected:
|
||||
self.connect()
|
||||
|
||||
self._telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
|
||||
neighbors = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
if self._ap:
|
||||
self._telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
|
||||
arp_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||
leases_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
self._telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
|
||||
nvram_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1].split(b'<')[1:])
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except EOFError:
|
||||
_LOGGER.error("Unexpected response from router")
|
||||
self.disconnect()
|
||||
return None
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.error("Connection refused by router. Telnet enabled?")
|
||||
self.disconnect()
|
||||
return None
|
||||
except socket.gaierror as exc:
|
||||
_LOGGER.error("Socket exception: %s", exc)
|
||||
self.disconnect()
|
||||
return None
|
||||
except OSError as exc:
|
||||
_LOGGER.error("OSError: %s", exc)
|
||||
self.disconnect()
|
||||
return None
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the ASUS-WRT Telnet server."""
|
||||
self._telnet = telnetlib.Telnet(self._host)
|
||||
self._telnet.read_until(b'login: ')
|
||||
self._telnet.write((self._username + '\n').encode('ascii'))
|
||||
self._telnet.read_until(b'Password: ')
|
||||
self._telnet.write((self._password + '\n').encode('ascii'))
|
||||
self._prompt_string = self._telnet.read_until(b'#').split(b'\n')[-1]
|
||||
|
||||
super(TelnetConnection, self).connect()
|
||||
|
||||
def disconnect(self): \
|
||||
# pylint: disable=broad-except
|
||||
"""Disconnect the current Telnet connection."""
|
||||
try:
|
||||
self._telnet.write('exit\n'.encode('ascii'))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
super(TelnetConnection, self).disconnect()
|
||||
|
||||
@@ -39,7 +39,7 @@ class GPSLoggerView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Handle for GPSLogger message received as GET."""
|
||||
res = yield from self._handle(request.app['hass'], request.GET)
|
||||
res = yield from self._handle(request.app['hass'], request.query)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -75,10 +75,10 @@ class GPSLoggerView(HomeAssistantView):
|
||||
if 'activity' in data:
|
||||
attrs['activity'] = data['activity']
|
||||
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
gps=gps_location, battery=battery,
|
||||
gps_accuracy=accuracy,
|
||||
attributes=attrs))
|
||||
yield from hass.async_add_job(
|
||||
partial(self.see, dev_id=device,
|
||||
gps=gps_location, battery=battery,
|
||||
gps_accuracy=accuracy,
|
||||
attributes=attrs))
|
||||
|
||||
return 'Setting location for {}'.format(device)
|
||||
|
||||
@@ -41,7 +41,7 @@ class LocativeView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Locative message received as GET."""
|
||||
res = yield from self._handle(request.app['hass'], request.GET)
|
||||
res = yield from self._handle(request.app['hass'], request.query)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -79,10 +79,9 @@ class LocativeView(HomeAssistantView):
|
||||
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||
|
||||
if direction == 'enter':
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
location_name=location_name,
|
||||
gps=gps_location))
|
||||
yield from hass.async_add_job(
|
||||
partial(self.see, dev_id=device, location_name=location_name,
|
||||
gps=gps_location))
|
||||
return 'Setting location to {}'.format(location_name)
|
||||
|
||||
elif direction == 'exit':
|
||||
@@ -91,10 +90,9 @@ class LocativeView(HomeAssistantView):
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
location_name = STATE_NOT_HOME
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
location_name=location_name,
|
||||
gps=gps_location))
|
||||
yield from hass.async_add_job(
|
||||
partial(self.see, dev_id=device,
|
||||
location_name=location_name, gps=gps_location))
|
||||
return 'Setting location to not home'
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
|
||||
@@ -60,13 +60,20 @@ class MikrotikScanner(DeviceScanner):
|
||||
self.success_init = False
|
||||
self.client = None
|
||||
|
||||
self.wireless_exist = None
|
||||
self.success_init = self.connect_to_device()
|
||||
|
||||
if self.success_init:
|
||||
_LOGGER.info("Start polling Mikrotik router...")
|
||||
_LOGGER.info(
|
||||
"Start polling Mikrotik (%s) router...",
|
||||
self.host
|
||||
)
|
||||
self._update_info()
|
||||
else:
|
||||
_LOGGER.error("Connection to Mikrotik failed")
|
||||
_LOGGER.error(
|
||||
"Connection to Mikrotik (%s) failed",
|
||||
self.host
|
||||
)
|
||||
|
||||
def connect_to_device(self):
|
||||
"""Connect to Mikrotik method."""
|
||||
@@ -87,6 +94,16 @@ class MikrotikScanner(DeviceScanner):
|
||||
routerboard_info[0].get('model', 'Router'),
|
||||
self.host)
|
||||
self.connected = True
|
||||
self.wireless_exist = self.client(
|
||||
cmd='/interface/wireless/getall'
|
||||
)
|
||||
if not self.wireless_exist:
|
||||
_LOGGER.info(
|
||||
'Mikrotik %s: Wireless adapters not found. Try to '
|
||||
'use DHCP lease table as presence tracker source. '
|
||||
'Please decrease lease time as much as possible.',
|
||||
self.host
|
||||
)
|
||||
|
||||
except (librouteros.exceptions.TrapError,
|
||||
librouteros.exceptions.ConnectionError) as api_error:
|
||||
@@ -108,24 +125,39 @@ class MikrotikScanner(DeviceScanner):
|
||||
def _update_info(self):
|
||||
"""Retrieve latest information from the Mikrotik box."""
|
||||
with self.lock:
|
||||
_LOGGER.info("Loading wireless device from Mikrotik...")
|
||||
if self.wireless_exist:
|
||||
devices_tracker = 'wireless'
|
||||
else:
|
||||
devices_tracker = 'ip'
|
||||
|
||||
wireless_clients = self.client(
|
||||
cmd='/interface/wireless/registration-table/getall'
|
||||
_LOGGER.info(
|
||||
"Loading %s devices from Mikrotik (%s) ...",
|
||||
devices_tracker,
|
||||
self.host
|
||||
)
|
||||
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
|
||||
|
||||
if device_names is None or wireless_clients is None:
|
||||
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
|
||||
if self.wireless_exist:
|
||||
devices = self.client(
|
||||
cmd='/interface/wireless/registration-table/getall'
|
||||
)
|
||||
else:
|
||||
devices = device_names
|
||||
|
||||
if device_names is None and devices is None:
|
||||
return False
|
||||
|
||||
mac_names = {device.get('mac-address'): device.get('host-name')
|
||||
for device in device_names
|
||||
if device.get('mac-address')}
|
||||
|
||||
self.last_results = {
|
||||
device.get('mac-address'):
|
||||
mac_names.get(device.get('mac-address'))
|
||||
for device in wireless_clients
|
||||
}
|
||||
if self.wireless_exist:
|
||||
self.last_results = {
|
||||
device.get('mac-address'):
|
||||
mac_names.get(device.get('mac-address'))
|
||||
for device in devices
|
||||
}
|
||||
else:
|
||||
self.last_results = mac_names
|
||||
|
||||
return True
|
||||
|
||||
@@ -116,7 +116,6 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"key for topic %s", topic)
|
||||
return None
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def validate_payload(topic, payload, data_type):
|
||||
"""Validate the OwnTracks payload."""
|
||||
try:
|
||||
|
||||
@@ -57,7 +57,7 @@ class Host(object):
|
||||
def update(self, see):
|
||||
"""Update device state by sending one or more ping messages."""
|
||||
failed = 0
|
||||
while failed < self._count: # check more times if host in unreachable
|
||||
while failed < self._count: # check more times if host is unreachable
|
||||
if self.ping():
|
||||
see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
|
||||
return True
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.3.5']
|
||||
REQUIREMENTS = ['pysnmp==4.3.8']
|
||||
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
|
||||
Regular → Executable
+4
-1
@@ -144,7 +144,10 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
|
||||
response = res.json()
|
||||
|
||||
if rpcmethod == "call":
|
||||
return response["result"][1]
|
||||
try:
|
||||
return response["result"][1]
|
||||
except IndexError:
|
||||
return
|
||||
else:
|
||||
return response["result"]
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.unifi/
|
||||
"""
|
||||
import logging
|
||||
import urllib
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -15,7 +14,7 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import CONF_VERIFY_SSL
|
||||
|
||||
REQUIREMENTS = ['pyunifi==2.12']
|
||||
REQUIREMENTS = ['pyunifi==2.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
@@ -40,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Set up the Unifi device_tracker."""
|
||||
from pyunifi.controller import Controller
|
||||
from pyunifi.controller import Controller, APIError
|
||||
|
||||
host = config[DOMAIN].get(CONF_HOST)
|
||||
username = config[DOMAIN].get(CONF_USERNAME)
|
||||
@@ -53,7 +52,7 @@ def get_scanner(hass, config):
|
||||
try:
|
||||
ctrl = Controller(host, username, password, port, version='v4',
|
||||
site_id=site_id, ssl_verify=verify_ssl)
|
||||
except urllib.error.HTTPError as ex:
|
||||
except APIError as ex:
|
||||
_LOGGER.error("Failed to connect to Unifi: %s", ex)
|
||||
persistent_notification.create(
|
||||
hass, 'Failed to connect to Unifi. '
|
||||
@@ -77,9 +76,10 @@ class UnifiScanner(DeviceScanner):
|
||||
|
||||
def _update(self):
|
||||
"""Get the clients from the device."""
|
||||
from pyunifi.controller import APIError
|
||||
try:
|
||||
clients = self._controller.get_clients()
|
||||
except urllib.error.HTTPError as ex:
|
||||
except APIError as ex:
|
||||
_LOGGER.error("Failed to scan clients: %s", ex)
|
||||
clients = []
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ https://home-assistant.io/components/device_tracker.volvooncall/
|
||||
import logging
|
||||
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.components.volvooncall import DOMAIN
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
dispatcher_connect, dispatcher_send)
|
||||
from homeassistant.components.volvooncall import (
|
||||
DATA_KEY, SIGNAL_VEHICLE_SEEN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,19 +21,19 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
return
|
||||
|
||||
vin, _ = discovery_info
|
||||
vehicle = hass.data[DOMAIN].vehicles[vin]
|
||||
|
||||
host_name = vehicle.registration_number
|
||||
dev_id = 'volvo_' + slugify(host_name)
|
||||
vehicle = hass.data[DATA_KEY].vehicles[vin]
|
||||
|
||||
def see_vehicle(vehicle):
|
||||
"""Handle the reporting of the vehicle position."""
|
||||
host_name = vehicle.registration_number
|
||||
dev_id = 'volvo_{}'.format(slugify(host_name))
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=(vehicle.position['latitude'],
|
||||
vehicle.position['longitude']))
|
||||
vehicle.position['longitude']),
|
||||
icon='mdi:car')
|
||||
|
||||
hass.data[DOMAIN].entities[vin].append(see_vehicle)
|
||||
see_vehicle(vehicle)
|
||||
dispatcher_connect(hass, SIGNAL_VEHICLE_SEEN, see_vehicle)
|
||||
dispatcher_send(hass, SIGNAL_VEHICLE_SEEN, vehicle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==1.0.0']
|
||||
REQUIREMENTS = ['netdisco==1.0.1']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
@@ -115,8 +115,7 @@ def async_setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def scan_devices(now):
|
||||
"""Scan for devices."""
|
||||
results = yield from hass.loop.run_in_executor(
|
||||
None, _discover, netdisco)
|
||||
results = yield from hass.async_add_job(_discover, netdisco)
|
||||
|
||||
for result in results:
|
||||
hass.async_add_job(new_service_found(*result))
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Parent component for Dyson Pure Cool Link devices."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \
|
||||
CONF_DEVICES
|
||||
|
||||
REQUIREMENTS = ['libpurecoollink==0.1.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_LANGUAGE = "language"
|
||||
CONF_RETRY = "retry"
|
||||
|
||||
DEFAULT_TIMEOUT = 5
|
||||
DEFAULT_RETRY = 10
|
||||
|
||||
DOMAIN = "dyson"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_LANGUAGE): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int,
|
||||
vol.Optional(CONF_DEVICES, default=[]):
|
||||
vol.All(cv.ensure_list, [dict]),
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
DYSON_DEVICES = "dyson_devices"
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Dyson parent component."""
|
||||
_LOGGER.info("Creating new Dyson component")
|
||||
|
||||
if DYSON_DEVICES not in hass.data:
|
||||
hass.data[DYSON_DEVICES] = []
|
||||
|
||||
from libpurecoollink.dyson import DysonAccount
|
||||
dyson_account = DysonAccount(config[DOMAIN].get(CONF_USERNAME),
|
||||
config[DOMAIN].get(CONF_PASSWORD),
|
||||
config[DOMAIN].get(CONF_LANGUAGE))
|
||||
|
||||
logged = dyson_account.login()
|
||||
|
||||
timeout = config[DOMAIN].get(CONF_TIMEOUT)
|
||||
retry = config[DOMAIN].get(CONF_RETRY)
|
||||
|
||||
if not logged:
|
||||
_LOGGER.error("Not connected to Dyson account. Unable to add devices")
|
||||
return False
|
||||
|
||||
_LOGGER.info("Connected to Dyson account")
|
||||
dyson_devices = dyson_account.devices()
|
||||
if CONF_DEVICES in config[DOMAIN] and config[DOMAIN].get(CONF_DEVICES):
|
||||
configured_devices = config[DOMAIN].get(CONF_DEVICES)
|
||||
for device in configured_devices:
|
||||
dyson_device = next((d for d in dyson_devices if
|
||||
d.serial == device["device_id"]), None)
|
||||
if dyson_device:
|
||||
connected = dyson_device.connect(None, device["device_ip"],
|
||||
timeout, retry)
|
||||
if connected:
|
||||
_LOGGER.info("Connected to device %s", dyson_device)
|
||||
hass.data[DYSON_DEVICES].append(dyson_device)
|
||||
else:
|
||||
_LOGGER.warning("Unable to connect to device %s",
|
||||
dyson_device)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Unable to find device %s in Dyson account",
|
||||
device["device_id"])
|
||||
else:
|
||||
# Not yet reliable
|
||||
for device in dyson_devices:
|
||||
_LOGGER.info("Trying to connect to device %s with timeout=%i "
|
||||
"and retry=%i", device, timeout, retry)
|
||||
connected = device.connect(None, None, timeout, retry)
|
||||
if connected:
|
||||
_LOGGER.info("Connected to device %s", device)
|
||||
hass.data[DYSON_DEVICES].append(device)
|
||||
else:
|
||||
_LOGGER.warning("Unable to connect to device %s", device)
|
||||
|
||||
# Start fan/sensors components
|
||||
if hass.data[DYSON_DEVICES]:
|
||||
_LOGGER.debug("Starting sensor/fan components")
|
||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "fan", DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
@@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
REQUIREMENTS = ['pyeight==0.0.5']
|
||||
REQUIREMENTS = ['pyeight==0.0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -159,8 +159,8 @@ def async_setup(hass, config):
|
||||
CONF_BINARY_SENSORS: binary_sensors,
|
||||
}, config))
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyenvisalink==2.0']
|
||||
REQUIREMENTS = ['pyenvisalink==2.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'fan'
|
||||
|
||||
DEPENDENCIES = ['group']
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
GROUP_NAME_ALL_FANS = 'all fans'
|
||||
@@ -73,7 +73,7 @@ FAN_TURN_ON_SCHEMA = vol.Schema({
|
||||
}) # type: dict
|
||||
|
||||
FAN_TURN_OFF_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids
|
||||
}) # type: dict
|
||||
|
||||
FAN_OSCILLATE_SCHEMA = vol.Schema({
|
||||
@@ -139,9 +139,7 @@ def turn_on(hass, entity_id: str=None, speed: str=None) -> None:
|
||||
|
||||
def turn_off(hass, entity_id: str=None) -> None:
|
||||
"""Turn all or specified fan off."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
}
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
@@ -218,8 +216,7 @@ def async_setup(hass, config: dict):
|
||||
if not fan.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.async_add_job(
|
||||
fan.async_update_ha_state(True))
|
||||
update_coro = hass.async_add_job(fan.async_update_ha_state(True))
|
||||
if hasattr(fan, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
@@ -229,8 +226,8 @@ def async_setup(hass, config: dict):
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
# Listen for fan service calls.
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service_name in SERVICE_TO_METHOD:
|
||||
@@ -256,7 +253,7 @@ class FanEntity(ToggleEntity):
|
||||
"""
|
||||
if speed is SPEED_OFF:
|
||||
return self.async_turn_off()
|
||||
return self.hass.loop.run_in_executor(None, self.set_speed, speed)
|
||||
return self.hass.async_add_job(self.set_speed, speed)
|
||||
|
||||
def set_direction(self: ToggleEntity, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
@@ -267,8 +264,7 @@ class FanEntity(ToggleEntity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_direction, direction)
|
||||
return self.hass.async_add_job(self.set_direction, direction)
|
||||
|
||||
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
||||
"""Turn on the fan."""
|
||||
@@ -281,8 +277,8 @@ class FanEntity(ToggleEntity):
|
||||
"""
|
||||
if speed is SPEED_OFF:
|
||||
return self.async_turn_off()
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.turn_on, speed, **kwargs))
|
||||
return self.hass.async_add_job(
|
||||
ft.partial(self.turn_on, speed, **kwargs))
|
||||
|
||||
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
|
||||
"""Oscillate the fan."""
|
||||
@@ -293,8 +289,7 @@ class FanEntity(ToggleEntity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.oscillate, oscillating)
|
||||
return self.hass.async_add_job(self.oscillate, oscillating)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -9,31 +9,36 @@ from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_OSCILLATE, SUPPORT_DIRECTION)
|
||||
from homeassistant.const import STATE_OFF
|
||||
|
||||
FAN_NAME = 'Living Room Fan'
|
||||
FAN_ENTITY_ID = 'fan.living_room_fan'
|
||||
|
||||
DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
|
||||
FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
|
||||
LIMITED_SUPPORT = SUPPORT_SET_SPEED
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Set up the demo fan platform."""
|
||||
add_devices_callback([
|
||||
DemoFan(hass, FAN_NAME, STATE_OFF),
|
||||
DemoFan(hass, "Living Room Fan", FULL_SUPPORT),
|
||||
DemoFan(hass, "Ceiling Fan", LIMITED_SUPPORT),
|
||||
])
|
||||
|
||||
|
||||
class DemoFan(FanEntity):
|
||||
"""A demonstration fan component."""
|
||||
|
||||
def __init__(self, hass, name: str, initial_state: str) -> None:
|
||||
def __init__(self, hass, name: str, supported_features: int) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.hass = hass
|
||||
self._speed = initial_state
|
||||
self.oscillating = False
|
||||
self.direction = "forward"
|
||||
self._supported_features = supported_features
|
||||
self._speed = STATE_OFF
|
||||
self.oscillating = None
|
||||
self.direction = None
|
||||
self._name = name
|
||||
|
||||
if supported_features & SUPPORT_OSCILLATE:
|
||||
self.oscillating = False
|
||||
if supported_features & SUPPORT_DIRECTION:
|
||||
self.direction = "forward"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get entity name."""
|
||||
@@ -88,4 +93,4 @@ class DemoFan(FanEntity):
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return DEMO_SUPPORT
|
||||
return self._supported_features
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
"""Support for Dyson Pure Cool link fan."""
|
||||
import logging
|
||||
import asyncio
|
||||
from os import path
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.fan import (FanEntity, SUPPORT_OSCILLATE,
|
||||
SUPPORT_SET_SPEED,
|
||||
DOMAIN)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.dyson import DYSON_DEVICES
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
DEPENDENCIES = ['dyson']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DYSON_FAN_DEVICES = "dyson_fan_devices"
|
||||
SERVICE_SET_NIGHT_MODE = 'dyson_set_night_mode'
|
||||
|
||||
DYSON_SET_NIGHT_MODE_SCHEMA = vol.Schema({
|
||||
vol.Required('entity_id'): cv.entity_id,
|
||||
vol.Required('night_mode'): cv.boolean
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Dyson fan components."""
|
||||
_LOGGER.info("Creating new Dyson fans")
|
||||
if DYSON_FAN_DEVICES not in hass.data:
|
||||
hass.data[DYSON_FAN_DEVICES] = []
|
||||
|
||||
# Get Dyson Devices from parent component
|
||||
for device in hass.data[DYSON_DEVICES]:
|
||||
dyson_entity = DysonPureCoolLinkDevice(hass, device)
|
||||
hass.data[DYSON_FAN_DEVICES].append(dyson_entity)
|
||||
|
||||
add_devices(hass.data[DYSON_FAN_DEVICES])
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def service_handle(service):
|
||||
"""Handle dyson services."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
night_mode = service.data.get('night_mode')
|
||||
fan_device = next([fan for fan in hass.data[DYSON_FAN_DEVICES] if
|
||||
fan.entity_id == entity_id].__iter__(), None)
|
||||
if fan_device is None:
|
||||
_LOGGER.warning("Unable to find Dyson fan device %s",
|
||||
str(entity_id))
|
||||
return
|
||||
|
||||
if service.service == SERVICE_SET_NIGHT_MODE:
|
||||
fan_device.night_mode(night_mode)
|
||||
|
||||
# Register dyson service(s)
|
||||
hass.services.register(DOMAIN, SERVICE_SET_NIGHT_MODE,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_SET_NIGHT_MODE),
|
||||
schema=DYSON_SET_NIGHT_MODE_SCHEMA)
|
||||
|
||||
|
||||
class DysonPureCoolLinkDevice(FanEntity):
|
||||
"""Representation of a Dyson fan."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialize the fan."""
|
||||
_LOGGER.info("Creating device %s", device.name)
|
||||
self.hass = hass
|
||||
self._device = device
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.async_add_job(
|
||||
self._device.add_message_listener, self.on_message)
|
||||
|
||||
def on_message(self, message):
|
||||
"""Called when new messages received from the fan."""
|
||||
_LOGGER.debug(
|
||||
"Message received for fan device %s : %s", self.name, message)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this fan."""
|
||||
return self._device.name
|
||||
|
||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan. Never called ??."""
|
||||
_LOGGER.debug("Set fan speed to: " + speed)
|
||||
from libpurecoollink.const import FanSpeed, FanMode
|
||||
if speed == FanSpeed.FAN_SPEED_AUTO.value:
|
||||
self._device.set_configuration(fan_mode=FanMode.AUTO)
|
||||
else:
|
||||
fan_speed = FanSpeed('{0:04d}'.format(int(speed)))
|
||||
self._device.set_configuration(fan_mode=FanMode.FAN,
|
||||
fan_speed=fan_speed)
|
||||
|
||||
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
||||
"""Turn on the fan."""
|
||||
_LOGGER.debug("Turn on fan %s with speed %s", self.name, speed)
|
||||
from libpurecoollink.const import FanSpeed, FanMode
|
||||
if speed:
|
||||
if speed == FanSpeed.FAN_SPEED_AUTO.value:
|
||||
self._device.set_configuration(fan_mode=FanMode.AUTO)
|
||||
else:
|
||||
fan_speed = FanSpeed('{0:04d}'.format(int(speed)))
|
||||
self._device.set_configuration(fan_mode=FanMode.FAN,
|
||||
fan_speed=fan_speed)
|
||||
else:
|
||||
# Speed not set, just turn on
|
||||
self._device.set_configuration(fan_mode=FanMode.FAN)
|
||||
|
||||
def turn_off(self: ToggleEntity, **kwargs) -> None:
|
||||
"""Turn off the fan."""
|
||||
_LOGGER.debug("Turn off fan %s", self.name)
|
||||
from libpurecoollink.const import FanMode
|
||||
self._device.set_configuration(fan_mode=FanMode.OFF)
|
||||
|
||||
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
|
||||
"""Turn on/off oscillating."""
|
||||
_LOGGER.debug("Turn oscillation %s for device %s", oscillating,
|
||||
self.name)
|
||||
from libpurecoollink.const import Oscillation
|
||||
|
||||
if oscillating:
|
||||
self._device.set_configuration(
|
||||
oscillation=Oscillation.OSCILLATION_ON)
|
||||
else:
|
||||
self._device.set_configuration(
|
||||
oscillation=Oscillation.OSCILLATION_OFF)
|
||||
|
||||
@property
|
||||
def oscillating(self):
|
||||
"""Return the oscillation state."""
|
||||
return self._device.state and self._device.state.oscillation == "ON"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the entity is on."""
|
||||
if self._device.state:
|
||||
return self._device.state.fan_state == "FAN"
|
||||
return False
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
if self._device.state:
|
||||
from libpurecoollink.const import FanSpeed
|
||||
if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value:
|
||||
return self._device.state.speed
|
||||
else:
|
||||
return int(self._device.state.speed)
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_direction(self):
|
||||
"""Return direction of the fan [forward, reverse]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_night_mode(self):
|
||||
"""Return Night mode."""
|
||||
return self._device.state.night_mode == "ON"
|
||||
|
||||
def night_mode(self: ToggleEntity, night_mode: bool) -> None:
|
||||
"""Turn fan in night mode."""
|
||||
_LOGGER.debug("Set %s night mode %s", self.name, night_mode)
|
||||
from libpurecoollink.const import NightMode
|
||||
if night_mode:
|
||||
self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON)
|
||||
else:
|
||||
self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_OFF)
|
||||
|
||||
@property
|
||||
def is_auto_mode(self):
|
||||
"""Return auto mode."""
|
||||
return self._device.state.fan_mode == "AUTO"
|
||||
|
||||
def auto_mode(self: ToggleEntity, auto_mode: bool) -> None:
|
||||
"""Turn fan in auto mode."""
|
||||
_LOGGER.debug("Set %s auto mode %s", self.name, auto_mode)
|
||||
from libpurecoollink.const import FanMode
|
||||
if auto_mode:
|
||||
self._device.set_configuration(fan_mode=FanMode.AUTO)
|
||||
else:
|
||||
self._device.set_configuration(fan_mode=FanMode.FAN)
|
||||
|
||||
@property
|
||||
def speed_list(self: ToggleEntity) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
from libpurecoollink.const import FanSpeed
|
||||
supported_speeds = [FanSpeed.FAN_SPEED_AUTO.value,
|
||||
int(FanSpeed.FAN_SPEED_1.value),
|
||||
int(FanSpeed.FAN_SPEED_2.value),
|
||||
int(FanSpeed.FAN_SPEED_3.value),
|
||||
int(FanSpeed.FAN_SPEED_4.value),
|
||||
int(FanSpeed.FAN_SPEED_5.value),
|
||||
int(FanSpeed.FAN_SPEED_6.value),
|
||||
int(FanSpeed.FAN_SPEED_7.value),
|
||||
int(FanSpeed.FAN_SPEED_8.value),
|
||||
int(FanSpeed.FAN_SPEED_9.value),
|
||||
int(FanSpeed.FAN_SPEED_10.value)]
|
||||
|
||||
return supported_speeds
|
||||
|
||||
@property
|
||||
def supported_features(self: ToggleEntity) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED
|
||||
@@ -58,7 +58,18 @@ set_direction:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of the entities to toggle
|
||||
exampl: 'fan.living_room'
|
||||
example: 'fan.living_room'
|
||||
direction:
|
||||
description: The direction to rotate
|
||||
example: 'left'
|
||||
|
||||
dyson_set_night_mode:
|
||||
description: Set the fan in night mode
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of the entities to enable/disable night mode
|
||||
example: 'fan.living_room'
|
||||
night_mode:
|
||||
description: Night mode status
|
||||
example: true
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Z-Wave platform that handles fans.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.zwave/
|
||||
"""
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED)
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPEED_LIST = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
SUPPORTED_FEATURES = SUPPORT_SET_SPEED
|
||||
|
||||
# Value will first be divided to an integer
|
||||
VALUE_TO_SPEED = {
|
||||
0: SPEED_OFF,
|
||||
1: SPEED_LOW,
|
||||
2: SPEED_MEDIUM,
|
||||
3: SPEED_HIGH,
|
||||
}
|
||||
|
||||
SPEED_TO_VALUE = {
|
||||
SPEED_OFF: 0,
|
||||
SPEED_LOW: 1,
|
||||
SPEED_MEDIUM: 50,
|
||||
SPEED_HIGH: 99,
|
||||
}
|
||||
|
||||
|
||||
def get_device(values, **kwargs):
|
||||
"""Create Z-Wave entity device."""
|
||||
return ZwaveFan(values)
|
||||
|
||||
|
||||
class ZwaveFan(zwave.ZWaveDeviceEntity, FanEntity):
|
||||
"""Representation of a Z-Wave fan."""
|
||||
|
||||
def __init__(self, values):
|
||||
"""Initialize the Z-Wave fan device."""
|
||||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Handle data changes for node values."""
|
||||
value = math.ceil(self.values.primary.data * 3 / 100)
|
||||
self._state = VALUE_TO_SPEED[value]
|
||||
|
||||
def set_speed(self, speed):
|
||||
"""Set the speed of the fan."""
|
||||
self.node.set_dimmer(
|
||||
self.values.primary.value_id, SPEED_TO_VALUE[speed])
|
||||
|
||||
def turn_on(self, speed=None, **kwargs):
|
||||
"""Turn the device on."""
|
||||
if speed is None:
|
||||
# Value 255 tells device to return to previous value
|
||||
self.node.set_dimmer(self.values.primary.value_id, 255)
|
||||
else:
|
||||
self.set_speed(speed)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
self.node.set_dimmer(self.values.primary.value_id, 0)
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
"""Return the current speed."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def speed_list(self):
|
||||
"""Get the list of available speeds."""
|
||||
return SPEED_LIST
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORTED_FEATURES
|
||||
@@ -89,8 +89,8 @@ def async_setup(hass, config):
|
||||
conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST)
|
||||
)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
# Register service
|
||||
|
||||
@@ -268,8 +268,8 @@ class IndexView(HomeAssistantView):
|
||||
no_auth = 'true'
|
||||
|
||||
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
||||
template = yield from hass.loop.run_in_executor(
|
||||
None, self.templates.get_template, 'index.html')
|
||||
template = yield from hass.async_add_job(
|
||||
self.templates.get_template, 'index.html')
|
||||
|
||||
# pylint is wrong
|
||||
# pylint: disable=no-member
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
|
||||
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
|
||||
"frontend.html": "fbb9d6bdd3d661db26cad9475a5e22f1",
|
||||
"mdi.html": "f407a5a57addbe93817ee1b244d33fbe",
|
||||
"frontend.html": "cca45decbed803e7f0ec0b4f6e18fe53",
|
||||
"mdi.html": "1a5ad9654c1f0e57440e30afd92846a5",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
|
||||
"panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1",
|
||||
@@ -18,6 +18,6 @@ FINGERPRINTS = {
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
|
||||
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
|
||||
"panels/ha-panel-zwave.html": "19336d2c50c91dd6a122acc0606ff10d",
|
||||
"panels/ha-panel-zwave.html": "92edac58dd52c297c761fd9acec7f436",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
||||
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.
@@ -31,6 +31,200 @@
|
||||
});
|
||||
this.selectedNodeAttrs = att.sort();
|
||||
},
|
||||
});</script><dom-module id="zwave-values" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node Values"><div class="device-picker"><paper-dropdown-menu label="Value" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedValue}}"><template is="dom-repeat" items="[[values]]" as="item"><paper-item>[[computeSelectCaption(item)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsValueSelected(selectedValue)]]"><div class="card-actions"><paper-input float-label="Value Name" type="text" value="{{newValueNameInput}}" placeholder="[[computeGetValueName(selectedValue)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_value" service-data="[[computeValueNameServiceData(newValueNameInput)]]">Rename Value</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
||||
is: 'zwave-values',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
values: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
selectedNode: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
selectedValue: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'hass-service-called': 'serviceCalled',
|
||||
},
|
||||
|
||||
serviceCalled: function (ev) {
|
||||
if (ev.detail.success) {
|
||||
var foo = this;
|
||||
setTimeout(function () {
|
||||
foo.refreshValues(foo.selectedNode);
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
computeSelectCaption: function (item) {
|
||||
return item.value.label + ' (Instance: ' + item.value.instance + ', Index: ' + item.value.index + ')';
|
||||
},
|
||||
|
||||
computeGetValueName: function (selectedValue) {
|
||||
return this.values[selectedValue].value.label;
|
||||
},
|
||||
|
||||
computeIsValueSelected: function (selectedValue) {
|
||||
return (!this.nodes || this.selectedNode === -1 || selectedValue === -1);
|
||||
},
|
||||
|
||||
refreshValues: function (selectedNode) {
|
||||
var valueData = [];
|
||||
this.hass.callApi('GET', 'zwave/values/' + this.nodes[selectedNode].attributes.node_id).then(function (values) {
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
valueData.push({ key, value });
|
||||
});
|
||||
this.values = valueData;
|
||||
this.selectedValueChanged(this.selectedValue);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
computeValueNameServiceData: function (newValueNameInput) {
|
||||
if (!this.selectedNode === -1 || this.selectedValue === -1) return -1;
|
||||
return {
|
||||
node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||
value_id: this.values[this.selectedValue].key,
|
||||
name: newValueNameInput,
|
||||
};
|
||||
},
|
||||
});</script><dom-module id="zwave-groups" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node group associations"><div class="device-picker"><paper-dropdown-menu label="Node to control" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedTargetNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsTargetNodeSelected(selectedTargetNode)]]"><div class="device-picker"><paper-dropdown-menu label="Group" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedGroup}}"><template is="dom-repeat" items="[[groups]]" as="state"><paper-item>[[computeSelectCaptionGroup(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[!computeIsGroupSelected(selectedGroup)]]"><div class="help-text"><span>Other Nodes in this group:</span><template is="dom-repeat" items="[[otherGroupNodes]]" as="state"><span>[[state]]</span></template></div><div class="help-text"><span>Max Associations:</span> <span>[[maxAssociations]]</span></div><div class="card-actions"><template is="dom-if" if="[[!noAssociationsLeft]]"><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "add")]]">Add To Group</ha-call-service-button></template><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "remove")]]">Remove From Group</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
||||
is: 'zwave-groups',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
groups: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
selectedNode: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
selectedTargetNode: {
|
||||
type: Number,
|
||||
value: -1
|
||||
},
|
||||
|
||||
selectedGroup: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'selectedGroupChanged'
|
||||
},
|
||||
|
||||
otherGroupNodes: {
|
||||
type: Array,
|
||||
value: -1,
|
||||
computed: 'computeOtherGroupNodes(selectedGroup)'
|
||||
},
|
||||
|
||||
maxAssociations: {
|
||||
type: String,
|
||||
value: '',
|
||||
computed: 'computeMaxAssociations(selectedGroup)'
|
||||
},
|
||||
|
||||
noAssociationsLeft: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
computed: 'computeAssociationsLeft(selectedGroup)'
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'hass-service-called': 'serviceCalled',
|
||||
},
|
||||
|
||||
serviceCalled: function (ev) {
|
||||
if (ev.detail.success) {
|
||||
var foo = this;
|
||||
setTimeout(function () {
|
||||
foo.refreshGroups(foo.selectedNode);
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
computeAssociationsLeft: function (selectedGroup) {
|
||||
if (selectedGroup === -1) return true;
|
||||
return (this.maxAssociations === this.otherGroupNodes.length);
|
||||
},
|
||||
|
||||
computeMaxAssociations: function (selectedGroup) {
|
||||
if (selectedGroup === -1) return -1;
|
||||
var maxAssociations = this.groups[selectedGroup].value.max_associations;
|
||||
if (!maxAssociations) return ['None'];
|
||||
return maxAssociations;
|
||||
},
|
||||
|
||||
computeOtherGroupNodes: function (selectedGroup) {
|
||||
if (selectedGroup === -1) return -1;
|
||||
var associations = Object.values(this.groups[selectedGroup].value.associations);
|
||||
if (!associations.length) return ['None'];
|
||||
return associations;
|
||||
},
|
||||
|
||||
computeSelectCaption: function (stateObj) {
|
||||
return window.hassUtil.computeStateName(stateObj) + ' (Node:' +
|
||||
stateObj.attributes.node_id + ' ' +
|
||||
stateObj.attributes.query_stage + ')';
|
||||
},
|
||||
|
||||
computeSelectCaptionGroup: function (stateObj) {
|
||||
return (stateObj.key + ': ' + stateObj.value.label);
|
||||
},
|
||||
|
||||
computeIsTargetNodeSelected: function (selectedTargetNode) {
|
||||
return (!this.nodes || selectedTargetNode === -1);
|
||||
},
|
||||
|
||||
computeIsGroupSelected: function (selectedGroup) {
|
||||
return (!this.nodes || this.selectedNode === -1 || selectedGroup === -1);
|
||||
},
|
||||
|
||||
computeAssocServiceData: function (selectedGroup, type) {
|
||||
if (!this.groups === -1 || selectedGroup === -1 || this.selectedNode === -1) return -1;
|
||||
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||
association: type,
|
||||
target_node_id: this.nodes[this.selectedTargetNode].attributes.node_id,
|
||||
group: this.groups[selectedGroup].key };
|
||||
},
|
||||
|
||||
refreshGroups: function (selectedNode) {
|
||||
var groupData = [];
|
||||
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
|
||||
Object.entries(groups).forEach(([key, value]) => {
|
||||
groupData.push({ key, value });
|
||||
});
|
||||
this.groups = groupData;
|
||||
this.selectedGroupChanged(this.selectedGroup);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
selectedGroupChanged: function (selectedGroup) {
|
||||
if (this.selectedGroup === -1 || selectedGroup === -1) return;
|
||||
this.maxAssociations = this.groups[selectedGroup].value.max_associations;
|
||||
this.otherGroupNodes = Object.values(this.groups[selectedGroup].value.associations);
|
||||
},
|
||||
});</script><dom-module id="zwave-node-config" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node config options"><template is="dom-if" if="[[wakeupNode]]"><div class="card-actions"><paper-input float-label="Wakeup Interval" type="number" value="{{wakeupInput}}" placeholder="[[computeGetWakeupValue(selectedNode)]]"><div suffix="">seconds</div></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_wakeup" service-data="[[computeWakeupServiceData(wakeupInput)]]">Set Wakeup</ha-call-service-button></div></template><div class="device-picker"><paper-dropdown-menu label="Config parameter" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedConfigParameter}}"><template is="dom-repeat" items="[[config]]" as="state"><paper-item>[[computeSelectCaptionConfigParameter(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'List')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Byte Short Int')]]"><div class="card-actions"><paper-input label="{{selectedConfigParameterNumValues}}" type="number" value="{{selectedConfigValue}}" max="{{configParameterMax}}" min="{{configParameterMin}}"></paper-input></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><div class="help-text"><span>[[configValueHelpText]]</span></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button Byte Short Int List')]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_config_parameter" service-data="[[computeSetConfigParameterServiceData(selectedConfigValue)]]">Set Config Parameter</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
||||
is: 'zwave-node-config',
|
||||
|
||||
@@ -313,131 +507,7 @@
|
||||
this.selectedUserCodeChanged(this.selectedUserCode);
|
||||
}.bind(this));
|
||||
},
|
||||
});</script><dom-module id="zwave-groups" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node group associations"><div class="device-picker"><paper-dropdown-menu label="Node to control" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedTargetNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsTargetNodeSelected(selectedTargetNode)]]"><div class="device-picker"><paper-dropdown-menu label="Group" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedGroup}}"><template is="dom-repeat" items="[[groups]]" as="state"><paper-item>[[computeSelectCaptionGroup(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[!computeIsGroupSelected(selectedGroup)]]"><div class="help-text"><span>Other Nodes in this group:</span><template is="dom-repeat" items="[[otherGroupNodes]]" as="state"><span>[[state]]</span></template></div><div class="help-text"><span>Max Associations:</span> <span>[[maxAssociations]]</span></div><div class="card-actions"><template is="dom-if" if="[[!noAssociationsLeft]]"><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "add")]]">Add To Group</ha-call-service-button></template><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "remove")]]">Remove From Group</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
||||
is: 'zwave-groups',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
groups: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
selectedNode: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
selectedTargetNode: {
|
||||
type: Number,
|
||||
value: -1
|
||||
},
|
||||
|
||||
selectedGroup: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'selectedGroupChanged'
|
||||
},
|
||||
|
||||
otherGroupNodes: {
|
||||
type: Array,
|
||||
value: -1,
|
||||
computed: 'computeOtherGroupNodes(selectedGroup)'
|
||||
},
|
||||
|
||||
maxAssociations: {
|
||||
type: String,
|
||||
value: '',
|
||||
computed: 'computeMaxAssociations(selectedGroup)'
|
||||
},
|
||||
|
||||
noAssociationsLeft: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
computed: 'computeAssociationsLeft(selectedGroup)'
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'hass-service-called': 'serviceCalled',
|
||||
},
|
||||
|
||||
serviceCalled: function (ev) {
|
||||
if (ev.detail.success) {
|
||||
var foo = this;
|
||||
setTimeout(function () {
|
||||
foo.refreshGroups(foo.selectedNode);
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
computeAssociationsLeft: function (selectedGroup) {
|
||||
if (selectedGroup === -1) return true;
|
||||
return (this.maxAssociations === this.otherGroupNodes.length);
|
||||
},
|
||||
|
||||
computeMaxAssociations: function (selectedGroup) {
|
||||
if (selectedGroup === -1) return -1;
|
||||
var maxAssociations = this.groups[selectedGroup].value.max_associations;
|
||||
if (!maxAssociations) return ['None'];
|
||||
return maxAssociations;
|
||||
},
|
||||
|
||||
computeOtherGroupNodes: function (selectedGroup) {
|
||||
if (selectedGroup === -1) return -1;
|
||||
var associations = Object.values(this.groups[selectedGroup].value.associations);
|
||||
if (!associations.length) return ['None'];
|
||||
return associations;
|
||||
},
|
||||
|
||||
computeSelectCaption: function (stateObj) {
|
||||
return window.hassUtil.computeStateName(stateObj) + ' (Node:' +
|
||||
stateObj.attributes.node_id + ' ' +
|
||||
stateObj.attributes.query_stage + ')';
|
||||
},
|
||||
|
||||
computeSelectCaptionGroup: function (stateObj) {
|
||||
return (stateObj.key + ': ' + stateObj.value.label);
|
||||
},
|
||||
|
||||
computeIsTargetNodeSelected: function (selectedTargetNode) {
|
||||
return (!this.nodes || selectedTargetNode === -1);
|
||||
},
|
||||
|
||||
computeIsGroupSelected: function (selectedGroup) {
|
||||
return (!this.nodes || this.selectedNode === -1 || selectedGroup === -1);
|
||||
},
|
||||
|
||||
computeAssocServiceData: function (selectedGroup, type) {
|
||||
if (!this.groups === -1 || selectedGroup === -1 || this.selectedNode === -1) return -1;
|
||||
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||
association: type,
|
||||
target_node_id: this.nodes[this.selectedTargetNode].attributes.node_id,
|
||||
group: this.groups[selectedGroup].key };
|
||||
},
|
||||
|
||||
refreshGroups: function (selectedNode) {
|
||||
var groupData = [];
|
||||
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
|
||||
Object.entries(groups).forEach(([key, value]) => {
|
||||
groupData.push({ key, value });
|
||||
});
|
||||
this.groups = groupData;
|
||||
this.selectedGroupChanged(this.selectedGroup);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
selectedGroupChanged: function (selectedGroup) {
|
||||
if (this.selectedGroup === -1 || selectedGroup === -1) return;
|
||||
this.maxAssociations = this.groups[selectedGroup].value.max_associations;
|
||||
this.otherGroupNodes = Object.values(this.groups[selectedGroup].value.associations);
|
||||
},
|
||||
});</script></div><dom-module id="ha-panel-zwave"><template><style include="iron-flex ha-style">.content{margin-top:24px}.node-info{margin-left:16px;text-transform:capitalize}.help-text{padding-left:24px;padding-right:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Z-Wave Manager</div></app-toolbar></app-header><div class="content"><zwave-network id="zwave-network" hass="[[hass]]"></zwave-network></div><div class="content"><paper-card heading="Z-Wave Node Management"><div class="card-content">Z-Wave Node controls.</div><div class="device-picker"><paper-dropdown-menu label="Nodes" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_node" service-data="[[computeNodeServiceData(selectedNode)]]">Refresh Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="remove_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Remove Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="replace_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Replace Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="print_node" service-data="[[computeNodeServiceData(selectedNode)]]">Print Node</ha-call-service-button></div><div class="card-actions"><paper-input float-label="New node name" type="text" value="{{newNodeNameInput}}" placeholder="[[computeGetNodeName(selectedNode)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_node" service-data="[[computeNodeNameServiceData(newNodeNameInput)]]">Rename Node</ha-call-service-button></div><div class="device-picker"><paper-dropdown-menu label="Entities of this node" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedEntity}}"><template is="dom-repeat" items="[[entities]]" as="state"><paper-item>[[computeSelectCaptionEnt(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsEntitySelected(selectedEntity)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_entity" service-data="[[computeRefreshEntityServiceData(selectedEntity)]]">Refresh Entity</ha-call-service-button></div><div class="content"><div class="card-actions"><paper-button toggles="" raised="" noink="" active="{{entityInfoActive}}">Entity Attributes</paper-button></div><template is="dom-if" if="{{entityInfoActive}}"><template is="dom-repeat" items="[[selectedEntityAttrs]]" as="state"><div class="node-info"><span>[[state]]</span></div></template></template></div></template></template></paper-card></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-information id="zwave-node-information" nodes="[[nodes]]" selected-node="[[selectedNode]]"></zwave-node-information></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-groups hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" groups="[[groups]]"></zwave-groups></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-config hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" config="[[config]]"></zwave-node-config></template><template is="dom-if" if="{{hasNodeUserCodes}}"><zwave-usercodes id="zwave-usercodes" hass="[[hass]]" nodes="[[nodes]]" user-codes="[[userCodes]]" selected-node="[[selectedNode]]"></zwave-usercodes></template><div class="content"><ozw-log id="ozw-log" hass="[[hass]]"></ozw-log></div></app-header-layout></template></dom-module><script>Polymer({
|
||||
});</script></div><dom-module id="ha-panel-zwave"><template><style include="iron-flex ha-style">.content{margin-top:24px}.node-info{margin-left:16px;text-transform:capitalize}.help-text{padding-left:24px;padding-right:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Z-Wave Manager</div></app-toolbar></app-header><div class="content"><zwave-network id="zwave-network" hass="[[hass]]"></zwave-network></div><div class="content"><paper-card heading="Z-Wave Node Management"><div class="card-content">Z-Wave Node controls.</div><div class="device-picker"><paper-dropdown-menu label="Nodes" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_node" service-data="[[computeNodeServiceData(selectedNode)]]">Refresh Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="remove_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Remove Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="replace_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Replace Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="print_node" service-data="[[computeNodeServiceData(selectedNode)]]">Print Node</ha-call-service-button></div><div class="card-actions"><paper-input float-label="New node name" type="text" value="{{newNodeNameInput}}" placeholder="[[computeGetNodeName(selectedNode)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_node" service-data="[[computeNodeNameServiceData(newNodeNameInput)]]">Rename Node</ha-call-service-button></div><div class="device-picker"><paper-dropdown-menu label="Entities of this node" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedEntity}}"><template is="dom-repeat" items="[[entities]]" as="state"><paper-item>[[computeSelectCaptionEnt(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsEntitySelected(selectedEntity)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_entity" service-data="[[computeRefreshEntityServiceData(selectedEntity)]]">Refresh Entity</ha-call-service-button></div><div class="content"><div class="card-actions"><paper-button toggles="" raised="" noink="" active="{{entityInfoActive}}">Entity Attributes</paper-button></div><template is="dom-if" if="{{entityInfoActive}}"><template is="dom-repeat" items="[[selectedEntityAttrs]]" as="state"><div class="node-info"><span>[[state]]</span></div></template></template></div></template></template></paper-card></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-information id="zwave-node-information" nodes="[[nodes]]" selected-node="[[selectedNode]]"></zwave-node-information></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-values hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" values="[[values]]"></zwave-values></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-groups hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" groups="[[groups]]"></zwave-groups></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-config hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" config="[[config]]"></zwave-node-config></template><template is="dom-if" if="{{hasNodeUserCodes}}"><zwave-usercodes id="zwave-usercodes" hass="[[hass]]" nodes="[[nodes]]" user-codes="[[userCodes]]" selected-node="[[selectedNode]]"></zwave-usercodes></template><div class="content"><ozw-log id="ozw-log" hass="[[hass]]"></ozw-log></div></app-header-layout></template></dom-module><script>Polymer({
|
||||
is: 'ha-panel-zwave',
|
||||
|
||||
properties: {
|
||||
@@ -493,6 +563,10 @@
|
||||
computed: 'computeSelectedEntityAttrs(selectedEntity)'
|
||||
},
|
||||
|
||||
values: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
groups: {
|
||||
type: Array,
|
||||
},
|
||||
@@ -559,6 +633,8 @@
|
||||
},
|
||||
|
||||
selectedNodeChanged: function (selectedNode) {
|
||||
this.newNodeNameInput = '';
|
||||
|
||||
if (selectedNode === -1) return;
|
||||
this.selectedConfigParameter = -1;
|
||||
this.selectedConfigParameterValue = -1;
|
||||
@@ -570,6 +646,13 @@
|
||||
});
|
||||
this.config = configData;
|
||||
}.bind(this));
|
||||
var valueData = [];
|
||||
this.hass.callApi('GET', 'zwave/values/' + this.nodes[selectedNode].attributes.node_id).then(function (values) {
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
valueData.push({ key, value });
|
||||
});
|
||||
this.values = valueData;
|
||||
}.bind(this));
|
||||
var groupData = [];
|
||||
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
|
||||
Object.entries(groups).forEach(([key, value]) => {
|
||||
@@ -630,9 +713,7 @@
|
||||
computeGetNodeName: function (selectedNode) {
|
||||
if (this.selectedNode === -1 ||
|
||||
!this.nodes[selectedNode].entity_id) return -1;
|
||||
var str = (this.nodes[selectedNode].entity_id);
|
||||
var name = str.replace('zwave.', '');
|
||||
return name;
|
||||
return this.nodes[selectedNode].attributes.node_name;
|
||||
},
|
||||
|
||||
computeNodeNameServiceData: function (newNodeNameInput) {
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -14,7 +14,8 @@ from homeassistant import config as conf_util, core as ha
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
|
||||
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE, SERVICE_RELOAD)
|
||||
STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN,
|
||||
ATTR_ASSUMED_STATE, SERVICE_RELOAD)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -30,13 +31,23 @@ CONF_ENTITIES = 'entities'
|
||||
CONF_VIEW = 'view'
|
||||
CONF_CONTROL = 'control'
|
||||
|
||||
ATTR_ADD_ENTITIES = 'add_entities'
|
||||
ATTR_AUTO = 'auto'
|
||||
ATTR_CONTROL = 'control'
|
||||
ATTR_ENTITIES = 'entities'
|
||||
ATTR_ICON = 'icon'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_OBJECT_ID = 'object_id'
|
||||
ATTR_ORDER = 'order'
|
||||
ATTR_VIEW = 'view'
|
||||
ATTR_VISIBLE = 'visible'
|
||||
ATTR_CONTROL = 'control'
|
||||
|
||||
SERVICE_SET_VISIBILITY = 'set_visibility'
|
||||
SERVICE_SET = 'set'
|
||||
SERVICE_REMOVE = 'remove'
|
||||
|
||||
CONTROL_TYPES = vol.In(['hidden', None])
|
||||
|
||||
SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_VISIBLE): cv.boolean
|
||||
@@ -44,6 +55,21 @@ SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({
|
||||
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
SET_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_OBJECT_ID): cv.slug,
|
||||
vol.Optional(ATTR_NAME): cv.string,
|
||||
vol.Optional(ATTR_VIEW): cv.boolean,
|
||||
vol.Optional(ATTR_ICON): cv.string,
|
||||
vol.Optional(ATTR_CONTROL): CONTROL_TYPES,
|
||||
vol.Optional(ATTR_VISIBLE): cv.boolean,
|
||||
vol.Exclusive(ATTR_ENTITIES, 'entities'): cv.entity_ids,
|
||||
vol.Exclusive(ATTR_ADD_ENTITIES, 'entities'): cv.entity_ids,
|
||||
})
|
||||
|
||||
REMOVE_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_OBJECT_ID): cv.slug,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -60,7 +86,7 @@ GROUP_SCHEMA = vol.Schema({
|
||||
CONF_VIEW: cv.boolean,
|
||||
CONF_NAME: cv.string,
|
||||
CONF_ICON: cv.icon,
|
||||
CONF_CONTROL: cv.string,
|
||||
CONF_CONTROL: CONTROL_TYPES,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@@ -69,7 +95,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
# List of ON/OFF state tuples for groupable states
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
|
||||
(STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED)]
|
||||
(STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED),
|
||||
(STATE_PROBLEM, STATE_OK)]
|
||||
|
||||
|
||||
def _get_group_on_off(state):
|
||||
@@ -99,10 +126,10 @@ def reload(hass):
|
||||
hass.add_job(async_reload, hass)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def async_reload(hass):
|
||||
"""Reload the automation from config."""
|
||||
yield from hass.services.async_call(DOMAIN, SERVICE_RELOAD)
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RELOAD))
|
||||
|
||||
|
||||
def set_visibility(hass, entity_id=None, visible=True):
|
||||
@@ -111,6 +138,46 @@ def set_visibility(hass, entity_id=None, visible=True):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_VISIBILITY, data)
|
||||
|
||||
|
||||
def set_group(hass, object_id, name=None, entity_ids=None, visible=None,
|
||||
icon=None, view=None, control=None, add=None):
|
||||
"""Create a new user group."""
|
||||
hass.add_job(
|
||||
async_set_group, hass, object_id, name, entity_ids, visible, icon,
|
||||
view, control, add)
|
||||
|
||||
|
||||
@callback
|
||||
def async_set_group(hass, object_id, name=None, entity_ids=None, visible=None,
|
||||
icon=None, view=None, control=None, add=None):
|
||||
"""Create a new user group."""
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
(ATTR_OBJECT_ID, object_id),
|
||||
(ATTR_NAME, name),
|
||||
(ATTR_ENTITIES, entity_ids),
|
||||
(ATTR_VISIBLE, visible),
|
||||
(ATTR_ICON, icon),
|
||||
(ATTR_VIEW, view),
|
||||
(ATTR_CONTROL, control),
|
||||
(ATTR_ADD_ENTITIES, add),
|
||||
] if value is not None
|
||||
}
|
||||
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SET, data))
|
||||
|
||||
|
||||
def remove(hass, name):
|
||||
"""Remove a user group."""
|
||||
hass.add_job(async_remove, hass, name)
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove(hass, object_id):
|
||||
"""Remove a user group."""
|
||||
data = {ATTR_OBJECT_ID: object_id}
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_REMOVE, data))
|
||||
|
||||
|
||||
def expand_entity_ids(hass, entity_ids):
|
||||
"""Return entity_ids with group entity ids replaced by their members.
|
||||
|
||||
@@ -170,38 +237,126 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
|
||||
def async_setup(hass, config):
|
||||
"""Set up all groups found definded in the configuration."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
service_groups = {}
|
||||
|
||||
yield from _async_process_config(hass, config, component)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, os.path.join(
|
||||
descriptions = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
def reload_service_handler(service):
|
||||
"""Remove all groups and load new ones from config."""
|
||||
conf = yield from component.async_prepare_reload()
|
||||
if conf is None:
|
||||
return
|
||||
yield from _async_process_config(hass, conf, component)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_RELOAD], schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def groups_service_handler(service):
|
||||
"""Handle dynamic group service functions."""
|
||||
object_id = service.data[ATTR_OBJECT_ID]
|
||||
|
||||
# new group
|
||||
if service.service == SERVICE_SET and object_id not in service_groups:
|
||||
entity_ids = service.data.get(ATTR_ENTITIES) or \
|
||||
service.data.get(ATTR_ADD_ENTITIES) or None
|
||||
|
||||
extra_arg = {attr: service.data[attr] for attr in (
|
||||
ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL
|
||||
) if service.data.get(attr) is not None}
|
||||
|
||||
new_group = yield from Group.async_create_group(
|
||||
hass, service.data.get(ATTR_NAME, object_id),
|
||||
object_id=object_id,
|
||||
entity_ids=entity_ids,
|
||||
user_defined=False,
|
||||
**extra_arg
|
||||
)
|
||||
|
||||
service_groups[object_id] = new_group
|
||||
return
|
||||
|
||||
# update group
|
||||
if service.service == SERVICE_SET:
|
||||
group = service_groups[object_id]
|
||||
need_update = False
|
||||
|
||||
if ATTR_ADD_ENTITIES in service.data:
|
||||
delta = service.data[ATTR_ADD_ENTITIES]
|
||||
entity_ids = set(group.tracking) | set(delta)
|
||||
yield from group.async_update_tracked_entity_ids(entity_ids)
|
||||
|
||||
if ATTR_ENTITIES in service.data:
|
||||
entity_ids = service.data[ATTR_ENTITIES]
|
||||
yield from group.async_update_tracked_entity_ids(entity_ids)
|
||||
|
||||
if ATTR_NAME in service.data:
|
||||
group.name = service.data[ATTR_NAME]
|
||||
need_update = True
|
||||
|
||||
if ATTR_VISIBLE in service.data:
|
||||
group.visible = service.data[ATTR_VISIBLE]
|
||||
need_update = True
|
||||
|
||||
if ATTR_ICON in service.data:
|
||||
group.icon = service.data[ATTR_ICON]
|
||||
need_update = True
|
||||
|
||||
if ATTR_CONTROL in service.data:
|
||||
group.control = service.data[ATTR_CONTROL]
|
||||
need_update = True
|
||||
|
||||
if ATTR_VIEW in service.data:
|
||||
group.view = service.data[ATTR_VIEW]
|
||||
need_update = True
|
||||
|
||||
if need_update:
|
||||
yield from group.async_update_ha_state()
|
||||
|
||||
return
|
||||
|
||||
# remove group
|
||||
if service.service == SERVICE_REMOVE:
|
||||
if object_id not in service_groups:
|
||||
_LOGGER.warning("Group '%s' not exists!", object_id)
|
||||
return
|
||||
|
||||
del_group = service_groups.pop(object_id)
|
||||
yield from del_group.async_stop()
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET, groups_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_SET], schema=SET_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_REMOVE, groups_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_REMOVE], schema=REMOVE_SERVICE_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def visibility_service_handler(service):
|
||||
"""Change visibility of a group."""
|
||||
visible = service.data.get(ATTR_VISIBLE)
|
||||
tasks = [group.async_set_visible(visible) for group
|
||||
in component.async_extract_from_service(service,
|
||||
expand_group=False)]
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
tasks = []
|
||||
for group in component.async_extract_from_service(service,
|
||||
expand_group=False):
|
||||
group.visible = visible
|
||||
tasks.append(group.async_update_ha_state())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_SET_VISIBILITY],
|
||||
schema=SET_VISIBILITY_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_RELOAD], schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -231,8 +386,8 @@ def _async_process_config(hass, config, component):
|
||||
class Group(Entity):
|
||||
"""Track a group of entity ids."""
|
||||
|
||||
def __init__(self, hass, name, order=None, user_defined=True, icon=None,
|
||||
view=False, control=None):
|
||||
def __init__(self, hass, name, order=None, visible=True, icon=None,
|
||||
view=False, control=None, user_defined=True):
|
||||
"""Initialize a group.
|
||||
|
||||
This Object has factory function for creation.
|
||||
@@ -240,31 +395,33 @@ class Group(Entity):
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
self._user_defined = user_defined
|
||||
self._order = order
|
||||
self._icon = icon
|
||||
self._view = view
|
||||
self.view = view
|
||||
self.tracking = []
|
||||
self.group_on = None
|
||||
self.group_off = None
|
||||
self.visible = visible
|
||||
self.control = control
|
||||
self._user_defined = user_defined
|
||||
self._order = order
|
||||
self._assumed_state = False
|
||||
self._async_unsub_state_changed = None
|
||||
self._visible = True
|
||||
self._control = control
|
||||
|
||||
@staticmethod
|
||||
def create_group(hass, name, entity_ids=None, user_defined=True,
|
||||
icon=None, view=False, control=None, object_id=None):
|
||||
visible=True, icon=None, view=False, control=None,
|
||||
object_id=None):
|
||||
"""Initialize a group."""
|
||||
return run_coroutine_threadsafe(
|
||||
Group.async_create_group(hass, name, entity_ids, user_defined,
|
||||
icon, view, control, object_id),
|
||||
Group.async_create_group(
|
||||
hass, name, entity_ids, user_defined, visible, icon, view,
|
||||
control, object_id),
|
||||
hass.loop).result()
|
||||
|
||||
@staticmethod
|
||||
@asyncio.coroutine
|
||||
def async_create_group(hass, name, entity_ids=None, user_defined=True,
|
||||
icon=None, view=False, control=None,
|
||||
visible=True, icon=None, view=False, control=None,
|
||||
object_id=None):
|
||||
"""Initialize a group.
|
||||
|
||||
@@ -273,8 +430,9 @@ class Group(Entity):
|
||||
group = Group(
|
||||
hass, name,
|
||||
order=len(hass.states.async_entity_ids(DOMAIN)),
|
||||
user_defined=user_defined, icon=icon, view=view,
|
||||
control=control)
|
||||
visible=visible, icon=icon, view=view, control=control,
|
||||
user_defined=user_defined
|
||||
)
|
||||
|
||||
group.entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, object_id or name, hass=hass)
|
||||
@@ -297,6 +455,11 @@ class Group(Entity):
|
||||
"""Return the name of the group."""
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
"""Set Group name."""
|
||||
self._name = value
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the group."""
|
||||
@@ -307,19 +470,16 @@ class Group(Entity):
|
||||
"""Return the icon of the group."""
|
||||
return self._icon
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_visible(self, visible):
|
||||
"""Change visibility of the group."""
|
||||
if self._visible != visible:
|
||||
self._visible = visible
|
||||
yield from self.async_update_ha_state()
|
||||
@icon.setter
|
||||
def icon(self, value):
|
||||
"""Set Icon for group."""
|
||||
self._icon = value
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""If group should be hidden or not."""
|
||||
# Visibility from set_visibility service overrides
|
||||
if self._visible:
|
||||
return not self._user_defined or self._view
|
||||
if self.visible and not self.view:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -331,10 +491,10 @@ class Group(Entity):
|
||||
}
|
||||
if not self._user_defined:
|
||||
data[ATTR_AUTO] = True
|
||||
if self._view:
|
||||
if self.view:
|
||||
data[ATTR_VIEW] = True
|
||||
if self._control:
|
||||
data[ATTR_CONTROL] = self._control
|
||||
if self.control:
|
||||
data[ATTR_CONTROL] = self.control
|
||||
return data
|
||||
|
||||
@property
|
||||
|
||||
@@ -16,7 +16,7 @@ from aiohttp.hdrs import CONTENT_TYPE
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
|
||||
@@ -139,7 +139,7 @@ class HassIOView(HomeAssistantView):
|
||||
|
||||
name = "api:hassio"
|
||||
url = "/api/hassio/{path:.+}"
|
||||
requires_auth = True
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hassio):
|
||||
"""Initialize a hassio base view."""
|
||||
@@ -148,6 +148,9 @@ class HassIOView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def _handle(self, request, path):
|
||||
"""Route data to hassio."""
|
||||
if path != 'panel' and not request[KEY_AUTHENTICATED]:
|
||||
return web.Response(status=401)
|
||||
|
||||
client = yield from self.hassio.command_proxy(path, request)
|
||||
|
||||
data = yield from client.read()
|
||||
|
||||
@@ -223,7 +223,7 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
if start_time > now:
|
||||
return self.json([])
|
||||
|
||||
end_time = request.GET.get('end_time')
|
||||
end_time = request.query.get('end_time')
|
||||
if end_time:
|
||||
end_time = dt_util.as_utc(
|
||||
dt_util.parse_datetime(end_time))
|
||||
@@ -231,11 +231,11 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
return self.json_message('Invalid end_time', HTTP_BAD_REQUEST)
|
||||
else:
|
||||
end_time = start_time + one_day
|
||||
entity_id = request.GET.get('filter_entity_id')
|
||||
entity_id = request.query.get('filter_entity_id')
|
||||
|
||||
result = yield from request.app['hass'].loop.run_in_executor(
|
||||
None, get_significant_states, request.app['hass'], start_time,
|
||||
end_time, entity_id, self.filters)
|
||||
result = yield from request.app['hass'].async_add_job(
|
||||
get_significant_states, request.app['hass'], start_time, end_time,
|
||||
entity_id, self.filters)
|
||||
result = result.values()
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
elapsed = time.perf_counter() - timer_start
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for Homematic devices.
|
||||
Support for HomeMatic devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/homematic/
|
||||
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.26']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.28']
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
|
||||
@@ -228,7 +228,7 @@ def set_var_value(hass, entity_id, value):
|
||||
|
||||
|
||||
def set_dev_value(hass, address, channel, param, value, proxy=None):
|
||||
"""Send virtual keypress to the Homematic controlller."""
|
||||
"""Call setValue XML-RPC method of supplied proxy."""
|
||||
data = {
|
||||
ATTR_ADDRESS: address,
|
||||
ATTR_CHANNEL: channel,
|
||||
@@ -245,16 +245,15 @@ def reconnect(hass):
|
||||
hass.services.call(DOMAIN, SERVICE_RECONNECT, {})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Set up the Homematic component."""
|
||||
from pyhomematic import HMConnection
|
||||
|
||||
hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY)
|
||||
hass.data[DATA_DEVINIT] = {}
|
||||
hass.data[DATA_STORE] = []
|
||||
hass.data[DATA_STORE] = set()
|
||||
|
||||
# Create hosts list for pyhomematic
|
||||
# Create hosts-dictionary for pyhomematic
|
||||
remotes = {}
|
||||
hosts = {}
|
||||
for rname, rconfig in config[DOMAIN][CONF_HOSTS].items():
|
||||
@@ -286,10 +285,10 @@ def setup(hass, config):
|
||||
interface_id='homeassistant'
|
||||
)
|
||||
|
||||
# Start server thread, connect to peer, initialize to receive events
|
||||
# Start server thread, connect to hosts, initialize to receive events
|
||||
hass.data[DATA_HOMEMATIC].start()
|
||||
|
||||
# Stops server when Homeassistant is shutting down
|
||||
# Stops server when HASS is shutting down
|
||||
hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop)
|
||||
|
||||
@@ -299,12 +298,12 @@ def setup(hass, config):
|
||||
entity_hubs.append(HMHub(
|
||||
hass, hub_data[CONF_NAME], hub_data[CONF_VARIABLES]))
|
||||
|
||||
# Register Homematic services
|
||||
# Register HomeMatic services
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def _hm_service_virtualkey(service):
|
||||
"""Service handle virtualkey services."""
|
||||
"""Service to handle virtualkey servicecalls."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
channel = service.data.get(ATTR_CHANNEL)
|
||||
param = service.data.get(ATTR_PARAM)
|
||||
@@ -315,18 +314,18 @@ def setup(hass, config):
|
||||
_LOGGER.error("%s not found for service virtualkey!", address)
|
||||
return
|
||||
|
||||
# If param exists for this device
|
||||
# Parameter doesn't exist for device
|
||||
if param not in hmdevice.ACTIONNODE:
|
||||
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
||||
return
|
||||
|
||||
# Channel exists?
|
||||
# Channel doesn't exist for device
|
||||
if channel not in hmdevice.ACTIONNODE[param]:
|
||||
_LOGGER.error("%i is not a channel in hm device %s",
|
||||
channel, address)
|
||||
return
|
||||
|
||||
# Call key
|
||||
# Call parameter
|
||||
hmdevice.actionNodeData(param, True, channel)
|
||||
|
||||
hass.services.register(
|
||||
@@ -335,7 +334,7 @@ def setup(hass, config):
|
||||
schema=SCHEMA_SERVICE_VIRTUALKEY)
|
||||
|
||||
def _service_handle_value(service):
|
||||
"""Set value on homematic variable."""
|
||||
"""Service to call setValue method for HomeMatic system variable."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
name = service.data[ATTR_NAME]
|
||||
value = service.data[ATTR_VALUE]
|
||||
@@ -347,7 +346,7 @@ def setup(hass, config):
|
||||
entities = entity_hubs
|
||||
|
||||
if not entities:
|
||||
_LOGGER.error("Homematic controller not found!")
|
||||
_LOGGER.error("No HomeMatic hubs available")
|
||||
return
|
||||
|
||||
for hub in entities:
|
||||
@@ -359,7 +358,7 @@ def setup(hass, config):
|
||||
schema=SCHEMA_SERVICE_SET_VAR_VALUE)
|
||||
|
||||
def _service_handle_reconnect(service):
|
||||
"""Reconnect to all homematic hubs."""
|
||||
"""Service to reconnect all HomeMatic hubs."""
|
||||
hass.data[DATA_HOMEMATIC].reconnect()
|
||||
|
||||
hass.services.register(
|
||||
@@ -368,7 +367,7 @@ def setup(hass, config):
|
||||
schema=SCHEMA_SERVICE_RECONNECT)
|
||||
|
||||
def _service_handle_device(service):
|
||||
"""Service handle set_dev_value services."""
|
||||
"""Service to call setValue method for HomeMatic devices."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
channel = service.data.get(ATTR_CHANNEL)
|
||||
param = service.data.get(ATTR_PARAM)
|
||||
@@ -380,7 +379,6 @@ def setup(hass, config):
|
||||
_LOGGER.error("%s not found!", address)
|
||||
return
|
||||
|
||||
# Call key
|
||||
hmdevice.setValue(param, value, channel)
|
||||
|
||||
hass.services.register(
|
||||
@@ -392,10 +390,9 @@ def setup(hass, config):
|
||||
|
||||
|
||||
def _system_callback_handler(hass, config, src, *args):
|
||||
"""Handle the callback."""
|
||||
"""System callback handler."""
|
||||
# New devices available at hub
|
||||
if src == 'newDevices':
|
||||
_LOGGER.debug("newDevices with: %s", args)
|
||||
# pylint: disable=unused-variable
|
||||
(interface_id, dev_descriptions) = args
|
||||
proxy = interface_id.split('-')[-1]
|
||||
|
||||
@@ -403,34 +400,25 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
if not hass.data[DATA_DEVINIT][proxy]:
|
||||
return
|
||||
|
||||
# Get list of all keys of the devices (ignoring channels)
|
||||
key_dict = {}
|
||||
addresses = []
|
||||
for dev in dev_descriptions:
|
||||
key_dict[dev['ADDRESS'].split(':')[0]] = True
|
||||
|
||||
# Remove device they allready init by HA
|
||||
tmp_devs = key_dict.copy()
|
||||
for dev in tmp_devs:
|
||||
if dev in hass.data[DATA_STORE]:
|
||||
del key_dict[dev]
|
||||
else:
|
||||
hass.data[DATA_STORE].append(dev)
|
||||
address = dev['ADDRESS'].split(':')[0]
|
||||
if address not in hass.data[DATA_STORE]:
|
||||
hass.data[DATA_STORE].add(address)
|
||||
addresses.append(address)
|
||||
|
||||
# Register EVENTS
|
||||
# Search all device with a EVENTNODE that include data
|
||||
# Search all devices with an EVENTNODE that includes data
|
||||
bound_event_callback = partial(_hm_event_handler, hass, proxy)
|
||||
for dev in key_dict:
|
||||
for dev in addresses:
|
||||
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev)
|
||||
|
||||
# Have events?
|
||||
if hmdevice.EVENTNODE:
|
||||
_LOGGER.debug("Register Events from %s", dev)
|
||||
hmdevice.setEventCallback(
|
||||
callback=bound_event_callback, bequeath=True)
|
||||
|
||||
# If configuration allows autodetection of devices,
|
||||
# all devices not configured are added.
|
||||
if key_dict:
|
||||
# Create HASS entities
|
||||
if addresses:
|
||||
for component_name, discovery_type in (
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
('light', DISCOVER_LIGHTS),
|
||||
@@ -440,18 +428,18 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
('climate', DISCOVER_CLIMATE)):
|
||||
# Get all devices of a specific type
|
||||
found_devices = _get_devices(
|
||||
hass, discovery_type, key_dict, proxy)
|
||||
hass, discovery_type, addresses, proxy)
|
||||
|
||||
# When devices of this type are found
|
||||
# they are setup in HA and an event is fired
|
||||
# they are setup in HASS and an discovery event is fired
|
||||
if found_devices:
|
||||
# Fire discovery event
|
||||
discovery.load_platform(hass, component_name, DOMAIN, {
|
||||
ATTR_DISCOVER_DEVICES: found_devices
|
||||
}, config)
|
||||
|
||||
# Homegear error message
|
||||
elif src == 'error':
|
||||
_LOGGER.debug("Error: %s", args)
|
||||
_LOGGER.error("Error: %s", args)
|
||||
(interface_id, errorcode, message) = args
|
||||
hass.bus.fire(EVENT_ERROR, {
|
||||
ATTR_ERRORCODE: errorcode,
|
||||
@@ -460,7 +448,7 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
|
||||
|
||||
def _get_devices(hass, discovery_type, keys, proxy):
|
||||
"""Get the Homematic devices for given discovery_type."""
|
||||
"""Get the HomeMatic devices for given discovery_type."""
|
||||
device_arr = []
|
||||
|
||||
for key in keys:
|
||||
@@ -468,11 +456,11 @@ def _get_devices(hass, discovery_type, keys, proxy):
|
||||
class_name = device.__class__.__name__
|
||||
metadata = {}
|
||||
|
||||
# Class supported by discovery type
|
||||
# Class not supported by discovery type
|
||||
if class_name not in HM_DEVICE_TYPES[discovery_type]:
|
||||
continue
|
||||
|
||||
# Load metadata if needed to generate a param list
|
||||
# Load metadata needed to generate a parameter list
|
||||
if discovery_type == DISCOVER_SENSORS:
|
||||
metadata.update(device.SENSORNODE)
|
||||
elif discovery_type == DISCOVER_BINARY_SENSORS:
|
||||
@@ -480,45 +468,41 @@ def _get_devices(hass, discovery_type, keys, proxy):
|
||||
else:
|
||||
metadata.update({None: device.ELEMENT})
|
||||
|
||||
if metadata:
|
||||
# Generate options for 1...n elements with 1...n params
|
||||
for param, channels in metadata.items():
|
||||
if param in HM_IGNORE_DISCOVERY_NODE:
|
||||
continue
|
||||
# Generate options for 1...n elements with 1...n parameters
|
||||
for param, channels in metadata.items():
|
||||
if param in HM_IGNORE_DISCOVERY_NODE:
|
||||
continue
|
||||
|
||||
# Add devices
|
||||
_LOGGER.debug("%s: Handling %s: %s: %s",
|
||||
discovery_type, key, param, channels)
|
||||
for channel in channels:
|
||||
name = _create_ha_name(
|
||||
name=device.NAME, channel=channel, param=param,
|
||||
count=len(channels)
|
||||
)
|
||||
device_dict = {
|
||||
CONF_PLATFORM: "homematic",
|
||||
ATTR_ADDRESS: key,
|
||||
ATTR_PROXY: proxy,
|
||||
ATTR_NAME: name,
|
||||
ATTR_CHANNEL: channel
|
||||
}
|
||||
if param is not None:
|
||||
device_dict[ATTR_PARAM] = param
|
||||
# Add devices
|
||||
_LOGGER.debug("%s: Handling %s: %s: %s",
|
||||
discovery_type, key, param, channels)
|
||||
for channel in channels:
|
||||
name = _create_ha_name(
|
||||
name=device.NAME, channel=channel, param=param,
|
||||
count=len(channels)
|
||||
)
|
||||
device_dict = {
|
||||
CONF_PLATFORM: "homematic",
|
||||
ATTR_ADDRESS: key,
|
||||
ATTR_PROXY: proxy,
|
||||
ATTR_NAME: name,
|
||||
ATTR_CHANNEL: channel
|
||||
}
|
||||
if param is not None:
|
||||
device_dict[ATTR_PARAM] = param
|
||||
|
||||
# Add new device
|
||||
try:
|
||||
DEVICE_SCHEMA(device_dict)
|
||||
device_arr.append(device_dict)
|
||||
except vol.MultipleInvalid as err:
|
||||
_LOGGER.error("Invalid device config: %s",
|
||||
str(err))
|
||||
else:
|
||||
_LOGGER.debug("Got no params for %s", key)
|
||||
_LOGGER.debug("%s autodiscovery done: %s", discovery_type, str(device_arr))
|
||||
# Add new device
|
||||
try:
|
||||
DEVICE_SCHEMA(device_dict)
|
||||
device_arr.append(device_dict)
|
||||
except vol.MultipleInvalid as err:
|
||||
_LOGGER.error("Invalid device config: %s",
|
||||
str(err))
|
||||
return device_arr
|
||||
|
||||
|
||||
def _create_ha_name(name, channel, param, count):
|
||||
"""Generate a unique object name."""
|
||||
"""Generate a unique entity id."""
|
||||
# HMDevice is a simple device
|
||||
if count == 1 and param is None:
|
||||
return name
|
||||
@@ -527,11 +511,11 @@ def _create_ha_name(name, channel, param, count):
|
||||
if count > 1 and param is None:
|
||||
return "{} {}".format(name, channel)
|
||||
|
||||
# With multiple param first elements
|
||||
# With multiple parameters on first channel
|
||||
if count == 1 and param is not None:
|
||||
return "{} {}".format(name, param)
|
||||
|
||||
# Multiple param on object with multiple elements
|
||||
# Multiple parameters with multiple channels
|
||||
if count > 1 and param is not None:
|
||||
return "{} {} {}".format(name, channel, param)
|
||||
|
||||
@@ -546,14 +530,14 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
|
||||
_LOGGER.error("Event handling channel convert error!")
|
||||
return
|
||||
|
||||
# is not a event?
|
||||
# Return if not an event supported by device
|
||||
if attribute not in hmdevice.EVENTNODE:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Event %s for %s channel %i", attribute,
|
||||
hmdevice.NAME, channel)
|
||||
|
||||
# keypress event
|
||||
# Keypress event
|
||||
if attribute in HM_PRESS_EVENTS:
|
||||
hass.bus.fire(EVENT_KEYPRESS, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
@@ -562,7 +546,7 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
|
||||
})
|
||||
return
|
||||
|
||||
# impulse event
|
||||
# Impulse event
|
||||
if attribute in HM_IMPULSE_EVENTS:
|
||||
hass.bus.fire(EVENT_IMPULSE, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
@@ -574,7 +558,7 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
|
||||
|
||||
|
||||
def _device_from_servicecall(hass, service):
|
||||
"""Extract homematic device from service call."""
|
||||
"""Extract HomeMatic device from service call."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
proxy = service.data.get(ATTR_PROXY)
|
||||
if address == 'BIDCOS-RF':
|
||||
@@ -589,10 +573,10 @@ def _device_from_servicecall(hass, service):
|
||||
|
||||
|
||||
class HMHub(Entity):
|
||||
"""The Homematic hub. I.e. CCU2/HomeGear."""
|
||||
"""The HomeMatic hub. (CCU2/HomeGear)."""
|
||||
|
||||
def __init__(self, hass, name, use_variables):
|
||||
"""Initialize Homematic hub."""
|
||||
"""Initialize HomeMatic hub."""
|
||||
self.hass = hass
|
||||
self.entity_id = "{}.{}".format(DOMAIN, name.lower())
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
@@ -601,7 +585,7 @@ class HMHub(Entity):
|
||||
self._state = STATE_UNKNOWN
|
||||
self._use_variables = use_variables
|
||||
|
||||
# load data
|
||||
# Load data
|
||||
track_time_interval(hass, self._update_hub, SCAN_INTERVAL_HUB)
|
||||
self._update_hub(None)
|
||||
|
||||
@@ -617,7 +601,7 @@ class HMHub(Entity):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. Homematic Hub object update variable."""
|
||||
"""Return false. HomeMatic Hub object updates variables."""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -660,7 +644,7 @@ class HMHub(Entity):
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def hm_set_variable(self, name, value):
|
||||
"""Set variable on homematic controller."""
|
||||
"""Set variable value on CCU/Homegear."""
|
||||
if name not in self._variables:
|
||||
_LOGGER.error("Variable %s not found on %s", name, self.name)
|
||||
return
|
||||
@@ -676,10 +660,10 @@ class HMHub(Entity):
|
||||
|
||||
|
||||
class HMDevice(Entity):
|
||||
"""The Homematic device base object."""
|
||||
"""The HomeMatic device base object."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a generic Homematic device."""
|
||||
"""Initialize a generic HomeMatic device."""
|
||||
self.hass = hass
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._name = config.get(ATTR_NAME)
|
||||
@@ -692,13 +676,13 @@ class HMDevice(Entity):
|
||||
self._connected = False
|
||||
self._available = False
|
||||
|
||||
# Set param to uppercase
|
||||
# Set parameter to uppercase
|
||||
if self._state:
|
||||
self._state = self._state.upper()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. Homematic states are pushed by the XML RPC Server."""
|
||||
"""Return false. HomeMatic states are pushed by the XML-RPC Server."""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -721,49 +705,44 @@ class HMDevice(Entity):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {}
|
||||
|
||||
# no data available to create
|
||||
# No data available
|
||||
if not self.available:
|
||||
return attr
|
||||
|
||||
# Generate an attributes list
|
||||
# Generate a dictionary with attributes
|
||||
for node, data in HM_ATTRIBUTE_SUPPORT.items():
|
||||
# Is an attributes and exists for this object
|
||||
# Is an attribute and exists for this object
|
||||
if node in self._data:
|
||||
value = data[1].get(self._data[node], self._data[node])
|
||||
attr[data[0]] = value
|
||||
|
||||
# static attributes
|
||||
# Static attributes
|
||||
attr['id'] = self._hmdevice.ADDRESS
|
||||
attr['proxy'] = self._proxy
|
||||
|
||||
return attr
|
||||
|
||||
def link_homematic(self):
|
||||
"""Connect to Homematic."""
|
||||
# Device is already linked
|
||||
"""Connect to HomeMatic."""
|
||||
if self._connected:
|
||||
return True
|
||||
|
||||
# Init
|
||||
# Initialize
|
||||
self._hmdevice = self._homematic.devices[self._proxy][self._address]
|
||||
self._connected = True
|
||||
|
||||
# Check if Homematic class is okay for HA class
|
||||
_LOGGER.info("Start linking %s to %s", self._address, self._name)
|
||||
try:
|
||||
# Init datapoints of this object
|
||||
# Initialize datapoints of this object
|
||||
self._init_data()
|
||||
if self.hass.data[DATA_DELAY]:
|
||||
# We delay / pause loading of data to avoid overloading
|
||||
# of CCU / Homegear when doing auto detection
|
||||
# We optionally delay / pause loading of data to avoid
|
||||
# overloading of CCU / Homegear
|
||||
time.sleep(self.hass.data[DATA_DELAY])
|
||||
self._load_data_from_hm()
|
||||
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
|
||||
|
||||
# Link events from pyhomatic
|
||||
# Link events from pyhomematic
|
||||
self._subscribe_homematic_events()
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
_LOGGER.debug("%s linking done", self._name)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
self._connected = False
|
||||
@@ -774,29 +753,28 @@ class HMDevice(Entity):
|
||||
"""Handle all pyhomematic device events."""
|
||||
_LOGGER.debug("%s received event '%s' value: %s", self._name,
|
||||
attribute, value)
|
||||
have_change = False
|
||||
has_changed = False
|
||||
|
||||
# Is data needed for this instance?
|
||||
if attribute in self._data:
|
||||
# Did data change?
|
||||
if self._data[attribute] != value:
|
||||
self._data[attribute] = value
|
||||
have_change = True
|
||||
has_changed = True
|
||||
|
||||
# If available it has changed
|
||||
# Availability has changed
|
||||
if attribute == 'UNREACH':
|
||||
self._available = bool(value)
|
||||
have_change = True
|
||||
has_changed = True
|
||||
|
||||
# If it has changed data point, update HA
|
||||
if have_change:
|
||||
_LOGGER.debug("%s update_ha_state after '%s'", self._name,
|
||||
attribute)
|
||||
# If it has changed data point, update HASS
|
||||
if has_changed:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _subscribe_homematic_events(self):
|
||||
"""Subscribe all required events to handle job."""
|
||||
channels_to_sub = {0: True} # add channel 0 for UNREACH
|
||||
channels_to_sub = set()
|
||||
channels_to_sub.add(0) # Add channel 0 for UNREACH
|
||||
|
||||
# Push data to channels_to_sub from hmdevice metadata
|
||||
for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE,
|
||||
@@ -814,8 +792,7 @@ class HMDevice(Entity):
|
||||
|
||||
# Prepare for subscription
|
||||
try:
|
||||
if int(channel) >= 0:
|
||||
channels_to_sub.update({int(channel): True})
|
||||
channels_to_sub.add(int(channel))
|
||||
except (ValueError, TypeError):
|
||||
_LOGGER.error("Invalid channel in metadata from %s",
|
||||
self._name)
|
||||
@@ -858,14 +835,14 @@ class HMDevice(Entity):
|
||||
return None
|
||||
|
||||
def _init_data(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
# Add all attributes to data dict
|
||||
"""Generate a data dict (self._data) from the HomeMatic metadata."""
|
||||
# Add all attributes to data dictionary
|
||||
for data_note in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data.update({data_note: STATE_UNKNOWN})
|
||||
|
||||
# init device specified data
|
||||
# Initialize device specific data
|
||||
self._init_data_struct()
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict from the Homematic device metadata."""
|
||||
"""Generate a data dictionary from the HomeMatic device metadata."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -51,7 +51,7 @@ CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
||||
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
|
||||
CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
|
||||
|
||||
# TLS configuation follows the best-practice guidelines specified here:
|
||||
# TLS configuration follows the best-practice guidelines specified here:
|
||||
# https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
# Intermediate guidelines are followed.
|
||||
SSL_VERSION = ssl.PROTOCOL_SSLv23
|
||||
@@ -339,7 +339,7 @@ class HomeAssistantWSGI(object):
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop(self):
|
||||
"""Stop the wsgi server."""
|
||||
"""Stop the WSGI server."""
|
||||
if self.server:
|
||||
self.server.close()
|
||||
yield from self.server.wait_closed()
|
||||
|
||||
@@ -37,8 +37,8 @@ def auth_middleware(app, handler):
|
||||
# A valid auth header has been set
|
||||
authenticated = True
|
||||
|
||||
elif (DATA_API_PASSWORD in request.GET and
|
||||
validate_password(request, request.GET[DATA_API_PASSWORD])):
|
||||
elif (DATA_API_PASSWORD in request.query and
|
||||
validate_password(request, request.query[DATA_API_PASSWORD])):
|
||||
authenticated = True
|
||||
|
||||
elif is_trusted_ip(request):
|
||||
|
||||
@@ -19,6 +19,8 @@ from .const import (
|
||||
KEY_FAILED_LOGIN_ATTEMPTS)
|
||||
from .util import get_real_ip
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NOTIFICATION_ID_BAN = 'ip-ban'
|
||||
NOTIFICATION_ID_LOGIN = 'http-login'
|
||||
|
||||
@@ -29,8 +31,6 @@ SCHEMA_IP_BAN_ENTRY = vol.Schema({
|
||||
vol.Optional('banned_at'): vol.Any(None, cv.datetime)
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def ban_middleware(app, handler):
|
||||
@@ -40,8 +40,8 @@ def ban_middleware(app, handler):
|
||||
|
||||
if KEY_BANNED_IPS not in app:
|
||||
hass = app['hass']
|
||||
app[KEY_BANNED_IPS] = yield from hass.loop.run_in_executor(
|
||||
None, load_ip_bans_config, hass.config.path(IP_BANS_FILE))
|
||||
app[KEY_BANNED_IPS] = yield from hass.async_add_job(
|
||||
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
|
||||
|
||||
@asyncio.coroutine
|
||||
def ban_middleware_handler(request):
|
||||
@@ -90,9 +90,8 @@ def process_wrong_login(request):
|
||||
request.app[KEY_BANNED_IPS].append(new_ban)
|
||||
|
||||
hass = request.app['hass']
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, update_ip_bans_config, hass.config.path(IP_BANS_FILE),
|
||||
new_ban)
|
||||
yield from hass.async_add_job(
|
||||
update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban)
|
||||
|
||||
_LOGGER.warning(
|
||||
"Banned IP %s for too many login attempts", remote_addr)
|
||||
|
||||
@@ -6,7 +6,7 @@ KEY_REAL_IP = 'ha_real_ip'
|
||||
KEY_BANS_ENABLED = 'ha_bans_enabled'
|
||||
KEY_BANNED_IPS = 'ha_banned_ips'
|
||||
KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts'
|
||||
KEY_LOGIN_THRESHOLD = 'ha_login_treshold'
|
||||
KEY_LOGIN_THRESHOLD = 'ha_login_threshold'
|
||||
KEY_DEVELOPMENT = 'ha_development'
|
||||
|
||||
HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For'
|
||||
|
||||
@@ -72,8 +72,8 @@ def async_setup(hass, config):
|
||||
|
||||
yield from component.async_setup(config)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -117,7 +117,7 @@ class ImageProcessingEntity(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(None, self.process_image, image)
|
||||
return self.hass.async_add_job(self.process_image, image)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
|
||||
@@ -7,22 +7,56 @@ https://home-assistant.io/components/image_processing.opencv/
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.components.image_processing import (
|
||||
ImageProcessingEntity, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.opencv import (
|
||||
ATTR_MATCHES, CLASSIFIER_GROUP_CONFIG, CONF_CLASSIFIER, CONF_ENTITY_ID,
|
||||
CONF_NAME, process_image)
|
||||
CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, PLATFORM_SCHEMA,
|
||||
ImageProcessingEntity)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['numpy==1.13.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['opencv']
|
||||
ATTR_MATCHES = 'matches'
|
||||
ATTR_TOTAL_MATCHES = 'total_matches'
|
||||
|
||||
CASCADE_URL = \
|
||||
'https://raw.githubusercontent.com/opencv/opencv/master/data/' + \
|
||||
'lbpcascades/lbpcascade_frontalface.xml'
|
||||
|
||||
CONF_CLASSIFIER = 'classifer'
|
||||
CONF_FILE = 'file'
|
||||
CONF_MIN_SIZE = 'min_size'
|
||||
CONF_NEIGHBORS = 'neighbors'
|
||||
CONF_SCALE = 'scale'
|
||||
|
||||
DEFAULT_CLASSIFIER_PATH = 'lbp_frontalface.xml'
|
||||
DEFAULT_MIN_SIZE = (30, 30)
|
||||
DEFAULT_NEIGHBORS = 4
|
||||
DEFAULT_SCALE = 1.1
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=2)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(CLASSIFIER_GROUP_CONFIG)
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_CLASSIFIER, default=None): {
|
||||
cv.string: vol.Any(
|
||||
cv.isfile,
|
||||
vol.Schema({
|
||||
vol.Required(CONF_FILE): cv.isfile,
|
||||
vol.Optional(CONF_SCALE, DEFAULT_SCALE): float,
|
||||
vol.Optional(CONF_NEIGHBORS, DEFAULT_NEIGHBORS):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_MIN_SIZE, DEFAULT_MIN_SIZE):
|
||||
vol.Schema((int, int))
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def _create_processor_from_config(hass, camera_entity, config):
|
||||
@@ -37,41 +71,63 @@ def _create_processor_from_config(hass, camera_entity, config):
|
||||
return processor
|
||||
|
||||
|
||||
def _get_default_classifier(dest_path):
|
||||
"""Download the default OpenCV classifier."""
|
||||
_LOGGER.info('Downloading default classifier')
|
||||
req = requests.get(CASCADE_URL, stream=True)
|
||||
with open(dest_path, 'wb') as fil:
|
||||
for chunk in req.iter_content(chunk_size=1024):
|
||||
if chunk: # filter out keep-alive new chunks
|
||||
fil.write(chunk)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the OpenCV image processing platform."""
|
||||
if discovery_info is None:
|
||||
try:
|
||||
# Verify opencv python package is preinstalled
|
||||
# pylint: disable=unused-import,unused-variable
|
||||
import cv2 # noqa
|
||||
except ImportError:
|
||||
_LOGGER.error("No opencv library found! " +
|
||||
"Install or compile for your system " +
|
||||
"following instructions here: " +
|
||||
"http://opencv.org/releases.html")
|
||||
return
|
||||
|
||||
devices = []
|
||||
for camera_entity in discovery_info[CONF_ENTITY_ID]:
|
||||
devices.append(
|
||||
_create_processor_from_config(hass, camera_entity, discovery_info))
|
||||
entities = []
|
||||
if config[CONF_CLASSIFIER] is None:
|
||||
dest_path = hass.config.path(DEFAULT_CLASSIFIER_PATH)
|
||||
_get_default_classifier(dest_path)
|
||||
config[CONF_CLASSIFIER] = {
|
||||
'Face': dest_path
|
||||
}
|
||||
|
||||
add_devices(devices)
|
||||
for camera in config[CONF_SOURCE]:
|
||||
entities.append(OpenCVImageProcessor(
|
||||
hass, camera[CONF_ENTITY_ID], camera.get(CONF_NAME),
|
||||
config[CONF_CLASSIFIER]
|
||||
))
|
||||
|
||||
add_devices(entities)
|
||||
|
||||
|
||||
class OpenCVImageProcessor(ImageProcessingEntity):
|
||||
"""Representation of an OpenCV image processor."""
|
||||
|
||||
def __init__(self, hass, camera_entity, name, classifier_configs):
|
||||
def __init__(self, hass, camera_entity, name, classifiers):
|
||||
"""Initialize the OpenCV entity."""
|
||||
self.hass = hass
|
||||
self._camera_entity = camera_entity
|
||||
self._name = name
|
||||
self._classifier_configs = classifier_configs
|
||||
if name:
|
||||
self._name = name
|
||||
else:
|
||||
self._name = "OpenCV {0}".format(
|
||||
split_entity_id(camera_entity)[1])
|
||||
self._classifiers = classifiers
|
||||
self._matches = {}
|
||||
self._total_matches = 0
|
||||
self._last_image = None
|
||||
|
||||
@property
|
||||
def last_image(self):
|
||||
"""Return the last image."""
|
||||
return self._last_image
|
||||
|
||||
@property
|
||||
def matches(self):
|
||||
"""Return the matches it found."""
|
||||
return self._matches
|
||||
|
||||
@property
|
||||
def camera_entity(self):
|
||||
"""Return camera entity id from process pictures."""
|
||||
@@ -85,20 +141,54 @@ class OpenCVImageProcessor(ImageProcessingEntity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
total_matches = 0
|
||||
for group in self._matches.values():
|
||||
total_matches += len(group)
|
||||
return total_matches
|
||||
return self._total_matches
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
return {
|
||||
ATTR_MATCHES: self._matches
|
||||
ATTR_MATCHES: self._matches,
|
||||
ATTR_TOTAL_MATCHES: self._total_matches
|
||||
}
|
||||
|
||||
def process_image(self, image):
|
||||
"""Process the image."""
|
||||
self._last_image = image
|
||||
self._matches = process_image(
|
||||
image, self._classifier_configs, False)
|
||||
import cv2 # pylint: disable=import-error
|
||||
import numpy
|
||||
|
||||
# pylint: disable=no-member
|
||||
cv_image = cv2.imdecode(numpy.asarray(bytearray(image)),
|
||||
cv2.IMREAD_UNCHANGED)
|
||||
|
||||
for name, classifier in self._classifiers.items():
|
||||
scale = DEFAULT_SCALE
|
||||
neighbors = DEFAULT_NEIGHBORS
|
||||
min_size = DEFAULT_MIN_SIZE
|
||||
if isinstance(classifier, dict):
|
||||
path = classifier[CONF_FILE]
|
||||
scale = classifier.get(CONF_SCALE, scale)
|
||||
neighbors = classifier.get(CONF_NEIGHBORS, neighbors)
|
||||
min_size = classifier.get(CONF_MIN_SIZE, min_size)
|
||||
else:
|
||||
path = classifier
|
||||
|
||||
# pylint: disable=no-member
|
||||
cascade = cv2.CascadeClassifier(path)
|
||||
|
||||
detections = cascade.detectMultiScale(
|
||||
cv_image,
|
||||
scaleFactor=scale,
|
||||
minNeighbors=neighbors,
|
||||
minSize=min_size)
|
||||
matches = {}
|
||||
total_matches = 0
|
||||
regions = []
|
||||
# pylint: disable=invalid-name
|
||||
for (x, y, w, h) in detections:
|
||||
regions.append((int(x), int(y), int(w), int(h)))
|
||||
total_matches += 1
|
||||
|
||||
matches[name] = regions
|
||||
|
||||
self._matches = matches
|
||||
self._total_matches = total_matches
|
||||
|
||||
@@ -20,7 +20,9 @@ from homeassistant.components.image_processing import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DIGITS = 'digits'
|
||||
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
|
||||
CONF_HEIGHT = 'height'
|
||||
CONF_ROTATE = 'rotate'
|
||||
CONF_SSOCR_BIN = 'ssocr_bin'
|
||||
CONF_THRESHOLD = 'threshold'
|
||||
CONF_WIDTH = 'width'
|
||||
@@ -30,10 +32,12 @@ CONF_Y_POS = 'y_position'
|
||||
DEFAULT_BINARY = 'ssocr'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS, default=''): cv.string,
|
||||
vol.Optional(CONF_DIGITS, default=-1): cv.positive_int,
|
||||
vol.Optional(CONF_HEIGHT, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_SSOCR_BIN, default=DEFAULT_BINARY): cv.string,
|
||||
vol.Optional(CONF_THRESHOLD, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_ROTATE, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_WIDTH, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_X_POS, default=0): cv.string,
|
||||
vol.Optional(CONF_Y_POS, default=0): cv.positive_int,
|
||||
@@ -65,14 +69,18 @@ class ImageProcessingSsocr(ImageProcessingEntity):
|
||||
self._name = "SevenSegement OCR {0}".format(
|
||||
split_entity_id(camera_entity)[1])
|
||||
self._state = None
|
||||
|
||||
self.filepath = os.path.join(self.hass.config.config_dir, 'ocr.png')
|
||||
self._command = [
|
||||
config[CONF_SSOCR_BIN], 'erosion', 'make_mono', 'crop',
|
||||
str(config[CONF_X_POS]), str(config[CONF_Y_POS]),
|
||||
str(config[CONF_WIDTH]), str(config[CONF_HEIGHT]), '-t',
|
||||
str(config[CONF_THRESHOLD]), '-d', str(config[CONF_DIGITS]),
|
||||
self.filepath
|
||||
]
|
||||
crop = ['crop', str(config[CONF_X_POS]), str(config[CONF_Y_POS]),
|
||||
str(config[CONF_WIDTH]), str(config[CONF_HEIGHT])]
|
||||
digits = ['-d', str(config[CONF_DIGITS])]
|
||||
rotate = ['rotate', str(config[CONF_ROTATE])]
|
||||
threshold = ['-t', str(config[CONF_THRESHOLD])]
|
||||
extra_arguments = config[CONF_EXTRA_ARGUMENTS].split(' ')
|
||||
|
||||
self._command = [config[CONF_SSOCR_BIN]] + crop + digits + threshold +\
|
||||
rotate + extra_arguments
|
||||
self._command.append(self.filepath)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -6,6 +6,8 @@ https://home-assistant.io/components/influxdb/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -56,6 +58,9 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
RE_DIGIT_TAIL = re.compile(r'^[^\.]*\d+\.?\d+[^\.]*$')
|
||||
RE_DECIMAL = re.compile(r'[^\d.]+')
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the InfluxDB component."""
|
||||
@@ -96,7 +101,7 @@ def setup(hass, config):
|
||||
|
||||
try:
|
||||
influx = InfluxDBClient(**kwargs)
|
||||
influx.query("SHOW DIAGNOSTICS;", database=conf[CONF_DB_NAME])
|
||||
influx.query("SHOW SERIES LIMIT 1;", database=conf[CONF_DB_NAME])
|
||||
except exceptions.InfluxDBClientError as exc:
|
||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||
"check your entries in the configuration file and that "
|
||||
@@ -160,7 +165,12 @@ def setup(hass, config):
|
||||
json_body[0]['fields'][key] = float(value)
|
||||
except (ValueError, TypeError):
|
||||
new_key = "{}_str".format(key)
|
||||
json_body[0]['fields'][new_key] = str(value)
|
||||
new_value = str(value)
|
||||
json_body[0]['fields'][new_key] = new_value
|
||||
|
||||
if RE_DIGIT_TAIL.match(new_value):
|
||||
json_body[0]['fields'][key] = float(
|
||||
RE_DECIMAL.sub('', new_value))
|
||||
|
||||
json_body[0]['tags'].update(tags)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/insteon_local/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@@ -13,7 +14,7 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['insteonlocal==0.48']
|
||||
REQUIREMENTS = ['insteonlocal==0.52']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -47,7 +48,12 @@ def setup(hass, config):
|
||||
timeout = conf.get(CONF_TIMEOUT)
|
||||
|
||||
try:
|
||||
insteonhub = Hub(host, username, password, port, timeout, _LOGGER)
|
||||
if not os.path.exists(hass.config.path('.insteon_cache')):
|
||||
os.makedirs(hass.config.path('.insteon_cache'))
|
||||
|
||||
insteonhub = Hub(host, username, password, port, timeout, _LOGGER,
|
||||
hass.config.path('.insteon_cache'))
|
||||
|
||||
# Check for successful connection
|
||||
insteonhub.get_buffer_status()
|
||||
except requests.exceptions.ConnectTimeout:
|
||||
|
||||
@@ -167,7 +167,7 @@ IDENTIFY_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_PUSH_SOUNDS): list
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
CONFIGURATION_FILE = 'ios.conf'
|
||||
CONFIGURATION_FILE = '.ios.conf'
|
||||
|
||||
CONFIG_FILE = {ATTR_DEVICES: {}}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Support for Juicenet cloud.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/juicenet
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-juicenet==0.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'juicenet'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_ACCESS_TOKEN): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Juicenet component."""
|
||||
import pyjuicenet
|
||||
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
|
||||
hass.data[DOMAIN]['api'] = pyjuicenet.Api(access_token)
|
||||
|
||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||
return True
|
||||
|
||||
|
||||
class JuicenetDevice(Entity):
|
||||
"""Represent a base Juicenet device."""
|
||||
|
||||
def __init__(self, device, sensor_type, hass):
|
||||
"""Initialise the sensor."""
|
||||
self.hass = hass
|
||||
self.device = device
|
||||
self.type = sensor_type
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self.device.name()
|
||||
|
||||
def update(self):
|
||||
"""Update state of the device."""
|
||||
self.device.update_state()
|
||||
|
||||
@property
|
||||
def _manufacturer_device_id(self):
|
||||
"""Return the manufacturer device id."""
|
||||
return self.device.id()
|
||||
|
||||
@property
|
||||
def _token(self):
|
||||
"""Return the device API token."""
|
||||
return self.device.token()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return "{}-{}".format(self.device.id(), self.type)
|
||||
@@ -26,6 +26,7 @@ from homeassistant.helpers.restore_state import async_restore_state
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
DOMAIN = "light"
|
||||
DEPENDENCIES = ['group']
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
GROUP_NAME_ALL_LIGHTS = 'all lights'
|
||||
@@ -77,6 +78,8 @@ EFFECT_COLORLOOP = "colorloop"
|
||||
EFFECT_RANDOM = "random"
|
||||
EFFECT_WHITE = "white"
|
||||
|
||||
COLOR_GROUP = "Color descriptors"
|
||||
|
||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
@@ -98,17 +101,21 @@ VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
|
||||
|
||||
LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
ATTR_PROFILE: cv.string,
|
||||
vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string,
|
||||
ATTR_TRANSITION: VALID_TRANSITION,
|
||||
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_NAME: cv.string,
|
||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||
vol.Coerce(tuple)),
|
||||
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||
vol.Coerce(tuple)),
|
||||
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
|
||||
vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP):
|
||||
vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||
vol.Coerce(tuple)),
|
||||
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP):
|
||||
vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||
vol.Coerce(tuple)),
|
||||
vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Exclusive(ATTR_KELVIN, COLOR_GROUP):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
|
||||
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
||||
ATTR_EFFECT: cv.string,
|
||||
@@ -285,8 +292,8 @@ def async_setup(hass, config):
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
# Listen for light on and light off service calls.
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
@@ -341,8 +348,7 @@ class Profiles:
|
||||
return None
|
||||
return profiles
|
||||
|
||||
cls._all = yield from hass.loop.run_in_executor(
|
||||
None, load_profile_data, hass)
|
||||
cls._all = yield from hass.async_add_job(load_profile_data, hass)
|
||||
return cls._all is not None
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -12,10 +12,9 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE,
|
||||
EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
|
||||
SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light,
|
||||
PLATFORM_SCHEMA)
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP,
|
||||
EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
|
||||
SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['flux_led==0.19']
|
||||
@@ -27,10 +26,8 @@ ATTR_MODE = 'mode'
|
||||
|
||||
DOMAIN = 'flux_led'
|
||||
|
||||
SUPPORT_FLUX_LED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
|
||||
SUPPORT_RGB_COLOR)
|
||||
SUPPORT_FLUX_LED_RGBW = (SUPPORT_WHITE_VALUE | SUPPORT_EFFECT |
|
||||
SUPPORT_RGB_COLOR)
|
||||
SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
|
||||
SUPPORT_RGB_COLOR)
|
||||
|
||||
MODE_RGB = 'rgb'
|
||||
MODE_RGBW = 'rgbw'
|
||||
@@ -182,16 +179,7 @@ class FluxLight(Light):
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
if self._mode == MODE_RGB:
|
||||
return self._bulb.brightness
|
||||
return None # not used for RGBW
|
||||
|
||||
@property
|
||||
def white_value(self):
|
||||
"""Return the white value of this light between 0..255."""
|
||||
if self._mode == MODE_RGBW:
|
||||
return self._bulb.getRgbw()[3]
|
||||
return None # not used for RGB
|
||||
return self._bulb.brightness
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
@@ -201,11 +189,7 @@ class FluxLight(Light):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
if self._mode == MODE_RGBW:
|
||||
return SUPPORT_FLUX_LED_RGBW
|
||||
elif self._mode == MODE_RGB:
|
||||
return SUPPORT_FLUX_LED_RGB
|
||||
return 0
|
||||
return SUPPORT_FLUX_LED
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
@@ -219,23 +203,17 @@ class FluxLight(Light):
|
||||
|
||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
white_value = kwargs.get(ATTR_WHITE_VALUE)
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
|
||||
if rgb is not None and brightness is not None:
|
||||
self._bulb.setRgb(*tuple(rgb), brightness=brightness)
|
||||
elif rgb is not None and white_value is not None:
|
||||
self._bulb.setRgbw(*tuple(rgb), w=white_value)
|
||||
elif rgb is not None:
|
||||
# self.white_value and self.brightness are appropriately
|
||||
# returning None for MODE_RGB and MODE_RGBW respectively
|
||||
self._bulb.setRgbw(*tuple(rgb),
|
||||
w=self.white_value,
|
||||
brightness=self.brightness)
|
||||
self._bulb.setRgb(*tuple(rgb))
|
||||
elif brightness is not None:
|
||||
self._bulb.setRgb(*self.rgb_color, brightness=brightness)
|
||||
elif white_value is not None:
|
||||
self._bulb.setRgbw(*self.rgb_color, w=white_value)
|
||||
if self._mode == 'rgbw':
|
||||
self._bulb.setWarmWhite255(brightness)
|
||||
elif self._mode == 'rgb':
|
||||
(red, green, blue) = self._bulb.getRgb()
|
||||
self._bulb.setRgb(red, green, blue, brightness=brightness)
|
||||
elif effect == EFFECT_RANDOM:
|
||||
self._bulb.setRgb(random.randint(0, 255),
|
||||
random.randint(0, 255),
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for the LIFX platform that implements lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.lifx/
|
||||
"""
|
||||
import colorsys
|
||||
import logging
|
||||
import asyncio
|
||||
import sys
|
||||
@@ -24,8 +23,6 @@ from homeassistant.components.light import (
|
||||
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT,
|
||||
preprocess_turn_on_alternatives)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
|
||||
from homeassistant import util
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
@@ -37,7 +34,7 @@ from . import effects as lifx_effects
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['aiolifx==0.4.6']
|
||||
REQUIREMENTS = ['aiolifx==0.4.8']
|
||||
|
||||
UDP_BROADCAST_PORT = 56700
|
||||
|
||||
@@ -49,19 +46,15 @@ CONF_SERVER = 'server'
|
||||
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
|
||||
|
||||
ATTR_HSBK = 'hsbk'
|
||||
ATTR_INFRARED = 'infrared'
|
||||
ATTR_POWER = 'power'
|
||||
|
||||
BYTE_MAX = 255
|
||||
SHORT_MAX = 65535
|
||||
|
||||
SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
|
||||
SUPPORT_XY_COLOR | SUPPORT_TRANSITION | SUPPORT_EFFECT)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
|
||||
})
|
||||
|
||||
LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({
|
||||
ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
|
||||
ATTR_POWER: cv.boolean,
|
||||
})
|
||||
|
||||
@@ -203,15 +196,14 @@ class AwaitAioLIFX:
|
||||
return self.message
|
||||
|
||||
|
||||
def convert_rgb_to_hsv(rgb):
|
||||
"""Convert Home Assistant RGB values to HSV values."""
|
||||
red, green, blue = [_ / BYTE_MAX for _ in rgb]
|
||||
def convert_8_to_16(value):
|
||||
"""Scale an 8 bit level into 16 bits."""
|
||||
return (value << 8) | value
|
||||
|
||||
hue, saturation, brightness = colorsys.rgb_to_hsv(red, green, blue)
|
||||
|
||||
return [int(hue * SHORT_MAX),
|
||||
int(saturation * SHORT_MAX),
|
||||
int(brightness * SHORT_MAX)]
|
||||
def convert_16_to_8(value):
|
||||
"""Scale a 16 bit level into 8 bits."""
|
||||
return value >> 8
|
||||
|
||||
|
||||
class LIFXLight(Light):
|
||||
@@ -229,6 +221,12 @@ class LIFXLight(Light):
|
||||
self.set_power(device.power_level)
|
||||
self.set_color(*device.color)
|
||||
|
||||
@property
|
||||
def lifxwhite(self):
|
||||
"""Return whether this is a white-only bulb."""
|
||||
# https://lan.developer.lifx.com/docs/lifx-products
|
||||
return self.product in [10, 11, 18]
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return the availability of the device."""
|
||||
@@ -257,14 +255,14 @@ class LIFXLight(Light):
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
brightness = int(self._bri / (BYTE_MAX + 1))
|
||||
brightness = convert_16_to_8(self._bri)
|
||||
_LOGGER.debug("brightness: %d", brightness)
|
||||
return brightness
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature."""
|
||||
temperature = color_temperature_kelvin_to_mired(self._kel)
|
||||
temperature = color_util.color_temperature_kelvin_to_mired(self._kel)
|
||||
|
||||
_LOGGER.debug("color_temp: %d", temperature)
|
||||
return temperature
|
||||
@@ -273,23 +271,21 @@ class LIFXLight(Light):
|
||||
def min_mireds(self):
|
||||
"""Return the coldest color_temp that this light supports."""
|
||||
# The 3 LIFX "White" products supported a limited temperature range
|
||||
# https://lan.developer.lifx.com/docs/lifx-products
|
||||
if self.product in [10, 11, 18]:
|
||||
if self.lifxwhite:
|
||||
kelvin = 6500
|
||||
else:
|
||||
kelvin = 9000
|
||||
return math.floor(color_temperature_kelvin_to_mired(kelvin))
|
||||
return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin))
|
||||
|
||||
@property
|
||||
def max_mireds(self):
|
||||
"""Return the warmest color_temp that this light supports."""
|
||||
# The 3 LIFX "White" products supported a limited temperature range
|
||||
# https://lan.developer.lifx.com/docs/lifx-products
|
||||
if self.product in [10, 11, 18]:
|
||||
if self.lifxwhite:
|
||||
kelvin = 2700
|
||||
else:
|
||||
kelvin = 2500
|
||||
return math.ceil(color_temperature_kelvin_to_mired(kelvin))
|
||||
return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin))
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -305,12 +301,18 @@ class LIFXLight(Light):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_LIFX
|
||||
features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
|
||||
SUPPORT_TRANSITION | SUPPORT_EFFECT)
|
||||
|
||||
if not self.lifxwhite:
|
||||
features |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR
|
||||
|
||||
return features
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
"""Return the list of supported effects."""
|
||||
return lifx_effects.effect_list()
|
||||
return lifx_effects.effect_list(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def update_after_transition(self, now):
|
||||
@@ -363,6 +365,9 @@ class LIFXLight(Light):
|
||||
yield from lifx_effects.default_effect(self, **kwargs)
|
||||
return
|
||||
|
||||
if ATTR_INFRARED in kwargs:
|
||||
self.device.set_infrared(convert_8_to_16(kwargs[ATTR_INFRARED]))
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||
else:
|
||||
@@ -439,7 +444,9 @@ class LIFXLight(Light):
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
hue, saturation, brightness = \
|
||||
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
|
||||
color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR])
|
||||
saturation = convert_8_to_16(saturation)
|
||||
brightness = convert_8_to_16(brightness)
|
||||
changed_color = True
|
||||
else:
|
||||
hue = self._hue
|
||||
@@ -448,12 +455,12 @@ class LIFXLight(Light):
|
||||
|
||||
if ATTR_XY_COLOR in kwargs:
|
||||
hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
|
||||
saturation = saturation * (BYTE_MAX + 1)
|
||||
saturation = convert_8_to_16(saturation)
|
||||
changed_color = True
|
||||
|
||||
# When color or temperature is set, use a default value for the other
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
kelvin = int(color_temperature_mired_to_kelvin(
|
||||
kelvin = int(color_util.color_temperature_mired_to_kelvin(
|
||||
kwargs[ATTR_COLOR_TEMP]))
|
||||
if not changed_color:
|
||||
saturation = 0
|
||||
@@ -465,7 +472,7 @@ class LIFXLight(Light):
|
||||
kelvin = self._kel
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
|
||||
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
|
||||
changed_color = True
|
||||
else:
|
||||
brightness = self._bri
|
||||
@@ -484,12 +491,8 @@ class LIFXLight(Light):
|
||||
self._bri = bri
|
||||
self._kel = kel
|
||||
|
||||
red, green, blue = colorsys.hsv_to_rgb(
|
||||
hue / SHORT_MAX, sat / SHORT_MAX, bri / SHORT_MAX)
|
||||
|
||||
red = int(red * BYTE_MAX)
|
||||
green = int(green * BYTE_MAX)
|
||||
blue = int(blue * BYTE_MAX)
|
||||
red, green, blue = color_util.color_hsv_to_RGB(
|
||||
hue, convert_16_to_8(sat), convert_16_to_8(bri))
|
||||
|
||||
_LOGGER.debug("set_color: %d %d %d %d [%d %d %d]",
|
||||
hue, sat, bri, kel, red, green, blue)
|
||||
|
||||
@@ -7,7 +7,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
|
||||
ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_TRANSITION,
|
||||
ATTR_RGB_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_EFFECT, ATTR_TRANSITION,
|
||||
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
|
||||
from homeassistant.const import (ATTR_ENTITY_ID)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -22,9 +22,18 @@ SERVICE_EFFECT_STOP = 'lifx_effect_stop'
|
||||
ATTR_POWER_ON = 'power_on'
|
||||
ATTR_PERIOD = 'period'
|
||||
ATTR_CYCLES = 'cycles'
|
||||
ATTR_MODE = 'mode'
|
||||
ATTR_SPREAD = 'spread'
|
||||
ATTR_CHANGE = 'change'
|
||||
|
||||
MODE_BLINK = 'blink'
|
||||
MODE_BREATHE = 'breathe'
|
||||
MODE_PING = 'ping'
|
||||
MODE_STROBE = 'strobe'
|
||||
MODE_SOLID = 'solid'
|
||||
|
||||
MODES = [MODE_BLINK, MODE_BREATHE, MODE_PING, MODE_STROBE, MODE_SOLID]
|
||||
|
||||
# aiolifx waveform modes
|
||||
WAVEFORM_SINE = 1
|
||||
WAVEFORM_PULSE = 4
|
||||
@@ -42,13 +51,15 @@ LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
||||
ATTR_COLOR_NAME: cv.string,
|
||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||
vol.Coerce(tuple)),
|
||||
vol.Optional(ATTR_PERIOD, default=1.0):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0.05)),
|
||||
vol.Optional(ATTR_CYCLES, default=1.0):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=1)),
|
||||
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)),
|
||||
ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)),
|
||||
})
|
||||
|
||||
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA
|
||||
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA.extend({
|
||||
vol.Optional(ATTR_MODE, default=MODE_BLINK): vol.In(MODES),
|
||||
})
|
||||
|
||||
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
||||
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||
@@ -131,14 +142,21 @@ def default_effect(light, **kwargs):
|
||||
yield from light.hass.services.async_call(DOMAIN, service, data)
|
||||
|
||||
|
||||
def effect_list():
|
||||
"""Return the list of supported effects."""
|
||||
return [
|
||||
SERVICE_EFFECT_COLORLOOP,
|
||||
SERVICE_EFFECT_BREATHE,
|
||||
SERVICE_EFFECT_PULSE,
|
||||
SERVICE_EFFECT_STOP,
|
||||
]
|
||||
def effect_list(light):
|
||||
"""Return the list of supported effects for this light."""
|
||||
if light.lifxwhite:
|
||||
return [
|
||||
SERVICE_EFFECT_BREATHE,
|
||||
SERVICE_EFFECT_PULSE,
|
||||
SERVICE_EFFECT_STOP,
|
||||
]
|
||||
else:
|
||||
return [
|
||||
SERVICE_EFFECT_COLORLOOP,
|
||||
SERVICE_EFFECT_BREATHE,
|
||||
SERVICE_EFFECT_PULSE,
|
||||
SERVICE_EFFECT_STOP,
|
||||
]
|
||||
|
||||
|
||||
class LIFXEffectData(object):
|
||||
@@ -208,14 +226,13 @@ class LIFXEffect(object):
|
||||
return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE]
|
||||
|
||||
|
||||
class LIFXEffectBreathe(LIFXEffect):
|
||||
"""Representation of a breathe effect."""
|
||||
class LIFXEffectPulse(LIFXEffect):
|
||||
"""Representation of a pulse effect."""
|
||||
|
||||
def __init__(self, hass, lights):
|
||||
"""Initialize the breathe effect."""
|
||||
super(LIFXEffectBreathe, self).__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_BREATHE
|
||||
self.waveform = WAVEFORM_SINE
|
||||
"""Initialize the pulse effect."""
|
||||
super().__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_PULSE
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_play(self, **kwargs):
|
||||
@@ -226,16 +243,47 @@ class LIFXEffectBreathe(LIFXEffect):
|
||||
@asyncio.coroutine
|
||||
def async_light_play(self, light, **kwargs):
|
||||
"""Play a light effect on the bulb."""
|
||||
period = kwargs[ATTR_PERIOD]
|
||||
cycles = kwargs[ATTR_CYCLES]
|
||||
hsbk, color_changed = light.find_hsbk(**kwargs)
|
||||
|
||||
# Default color is to fully (de)saturate with full brightness
|
||||
if kwargs[ATTR_MODE] == MODE_STROBE:
|
||||
# Strobe must flash from a dark color
|
||||
light.device.set_color([0, 0, 0, NEUTRAL_WHITE])
|
||||
yield from asyncio.sleep(0.1)
|
||||
default_period = 0.1
|
||||
default_cycles = 10
|
||||
else:
|
||||
default_period = 1.0
|
||||
default_cycles = 1
|
||||
|
||||
period = kwargs.get(ATTR_PERIOD, default_period)
|
||||
cycles = kwargs.get(ATTR_CYCLES, default_cycles)
|
||||
|
||||
# Breathe has a special waveform
|
||||
if kwargs[ATTR_MODE] == MODE_BREATHE:
|
||||
waveform = WAVEFORM_SINE
|
||||
else:
|
||||
waveform = WAVEFORM_PULSE
|
||||
|
||||
# Ping and solid have special duty cycles
|
||||
if kwargs[ATTR_MODE] == MODE_PING:
|
||||
ping_duration = int(5000 - min(2500, 300*period))
|
||||
duty_cycle = 2**15 - ping_duration
|
||||
elif kwargs[ATTR_MODE] == MODE_SOLID:
|
||||
duty_cycle = -2**15
|
||||
else:
|
||||
duty_cycle = 0
|
||||
|
||||
# Set default effect color based on current setting
|
||||
if not color_changed:
|
||||
if hsbk[1] > 65536/2:
|
||||
hsbk = [hsbk[0], 0, 65535, 4000]
|
||||
if kwargs[ATTR_MODE] == MODE_STROBE:
|
||||
# Strobe: cold white
|
||||
hsbk = [hsbk[0], 0, 65535, 5600]
|
||||
elif light.lifxwhite or hsbk[1] < 65536/2:
|
||||
# White: toggle brightness
|
||||
hsbk[2] = 65535 if hsbk[2] < 65536/2 else 0
|
||||
else:
|
||||
hsbk = [hsbk[0], 65535, 65535, hsbk[3]]
|
||||
# Color: fully desaturate with full brightness
|
||||
hsbk = [hsbk[0], 0, 65535, 4000]
|
||||
|
||||
# Start the effect
|
||||
args = {
|
||||
@@ -243,8 +291,8 @@ class LIFXEffectBreathe(LIFXEffect):
|
||||
'color': hsbk,
|
||||
'period': int(period*1000),
|
||||
'cycles': cycles,
|
||||
'duty_cycle': 0,
|
||||
'waveform': self.waveform,
|
||||
'duty_cycle': duty_cycle,
|
||||
'waveform': waveform,
|
||||
}
|
||||
light.device.set_waveform(args)
|
||||
|
||||
@@ -258,14 +306,21 @@ class LIFXEffectBreathe(LIFXEffect):
|
||||
return [hsbk[0], hsbk[1], 0, hsbk[2]]
|
||||
|
||||
|
||||
class LIFXEffectPulse(LIFXEffectBreathe):
|
||||
"""Representation of a pulse effect."""
|
||||
class LIFXEffectBreathe(LIFXEffectPulse):
|
||||
"""Representation of a breathe effect."""
|
||||
|
||||
def __init__(self, hass, lights):
|
||||
"""Initialize the pulse effect."""
|
||||
super(LIFXEffectPulse, self).__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_PULSE
|
||||
self.waveform = WAVEFORM_PULSE
|
||||
"""Initialize the breathe effect."""
|
||||
super().__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_BREATHE
|
||||
_LOGGER.warning("'lifx_effect_breathe' is deprecated. Please use "
|
||||
"'lifx_effect_pulse' with 'mode: breathe'")
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_perform(self, **kwargs):
|
||||
"""Prepare all lights for the effect."""
|
||||
kwargs[ATTR_MODE] = MODE_BREATHE
|
||||
yield from super().async_perform(**kwargs)
|
||||
|
||||
|
||||
class LIFXEffectColorloop(LIFXEffect):
|
||||
@@ -273,7 +328,7 @@ class LIFXEffectColorloop(LIFXEffect):
|
||||
|
||||
def __init__(self, hass, lights):
|
||||
"""Initialize the colorloop effect."""
|
||||
super(LIFXEffectColorloop, self).__init__(hass, lights)
|
||||
super().__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_COLORLOOP
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -324,7 +379,7 @@ class LIFXEffectStop(LIFXEffect):
|
||||
|
||||
def __init__(self, hass, lights):
|
||||
"""Initialize the stop effect."""
|
||||
super(LIFXEffectStop, self).__init__(hass, lights)
|
||||
super().__init__(hass, lights)
|
||||
self.name = SERVICE_EFFECT_STOP
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -9,6 +9,10 @@ lifx_set_state:
|
||||
'...':
|
||||
description: All turn_on parameters can be used to specify a color
|
||||
|
||||
infrared:
|
||||
description: Automatic infrared level (0..255) when light brightness is low
|
||||
example: 255
|
||||
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to the final state
|
||||
example: 10
|
||||
@@ -19,36 +23,7 @@ lifx_set_state:
|
||||
|
||||
|
||||
lifx_effect_breathe:
|
||||
description: Run a breathe effect by fading to a color and back.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to run the effect on
|
||||
example: 'light.kitchen'
|
||||
|
||||
brightness:
|
||||
description: Number between 0..255 indicating brightness when the effect peaks
|
||||
example: 120
|
||||
|
||||
color_name:
|
||||
description: A human readable color name
|
||||
example: 'red'
|
||||
|
||||
rgb_color:
|
||||
description: Color for the fade in RGB-format
|
||||
example: '[255, 100, 100]'
|
||||
|
||||
period:
|
||||
description: Duration of the effect in seconds (default 1.0)
|
||||
example: 3
|
||||
|
||||
cycles:
|
||||
description: Number of times the effect should run (default 1.0)
|
||||
example: 2
|
||||
|
||||
power_on:
|
||||
description: Powered off lights are temporarily turned on during the effect (default True)
|
||||
example: False
|
||||
description: Deprecated, use lifx_effect_pulse
|
||||
|
||||
lifx_effect_pulse:
|
||||
description: Run a flash effect by changing to a color and back.
|
||||
@@ -58,6 +33,10 @@ lifx_effect_pulse:
|
||||
description: Name(s) of entities to run the effect on
|
||||
example: 'light.kitchen'
|
||||
|
||||
mode:
|
||||
description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid'
|
||||
example: strobe
|
||||
|
||||
brightness:
|
||||
description: Number between 0..255 indicating brightness of the temporary color
|
||||
example: 120
|
||||
|
||||
@@ -11,14 +11,14 @@ from homeassistant.components.light import (
|
||||
from homeassistant.components.lutron import (
|
||||
LutronDevice, LUTRON_DEVICES, LUTRON_CONTROLLER)
|
||||
|
||||
DEPENDENCIES = ['lutron']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['lutron']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Lutron lights."""
|
||||
"""Set up the Lutron lights."""
|
||||
devs = []
|
||||
for (area_name, device) in hass.data[LUTRON_DEVICES]['light']:
|
||||
dev = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER])
|
||||
|
||||
@@ -157,8 +157,6 @@ class Luminary(Light):
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
self._luminary.set_onoff(1)
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
transition = int(kwargs[ATTR_TRANSITION] * 10)
|
||||
_LOGGER.debug("turn_on requested transition time for light: "
|
||||
@@ -168,6 +166,16 @@ class Luminary(Light):
|
||||
_LOGGER.debug("turn_on requested transition time for light: "
|
||||
"%s is: %s", self._name, transition)
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
_LOGGER.debug("turn_on requested brightness for light: %s is: %s ",
|
||||
self._name, self._brightness)
|
||||
self._luminary.set_luminance(
|
||||
int(self._brightness / 2.55),
|
||||
transition)
|
||||
else:
|
||||
self._luminary.set_onoff(1)
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
red, green, blue = kwargs[ATTR_RGB_COLOR]
|
||||
_LOGGER.debug("turn_on requested ATTR_RGB_COLOR for light:"
|
||||
@@ -191,14 +199,6 @@ class Luminary(Light):
|
||||
"%s: %s", self._name, kelvin)
|
||||
self._luminary.set_temperature(kelvin, transition)
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
_LOGGER.debug("turn_on requested brightness for light: %s is: %s ",
|
||||
self._name, self._brightness)
|
||||
self._luminary.set_luminance(
|
||||
int(self._brightness / 2.55),
|
||||
transition)
|
||||
|
||||
if ATTR_EFFECT in kwargs:
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
if effect == EFFECT_RANDOM:
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
Support for Template lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.template/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, STATE_ON,
|
||||
STATE_OFF, EVENT_HOMEASSISTANT_START, MATCH_ALL
|
||||
)
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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
|
||||
from homeassistant.helpers.script import Script
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_VALID_STATES = [STATE_ON, STATE_OFF, 'true', 'false']
|
||||
|
||||
CONF_LIGHTS = 'lights'
|
||||
CONF_ON_ACTION = 'turn_on'
|
||||
CONF_OFF_ACTION = 'turn_off'
|
||||
CONF_LEVEL_ACTION = 'set_level'
|
||||
CONF_LEVEL_TEMPLATE = 'level_template'
|
||||
|
||||
|
||||
LIGHT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE, default=None): cv.template,
|
||||
vol.Optional(CONF_LEVEL_ACTION, default=None): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_LEVEL_TEMPLATE, default=None): cv.template,
|
||||
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up Template Lights."""
|
||||
lights = []
|
||||
|
||||
for device, device_config in config[CONF_LIGHTS].items():
|
||||
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
|
||||
state_template = device_config[CONF_VALUE_TEMPLATE]
|
||||
on_action = device_config[CONF_ON_ACTION]
|
||||
off_action = device_config[CONF_OFF_ACTION]
|
||||
level_action = device_config.get(CONF_LEVEL_ACTION)
|
||||
level_template = device_config[CONF_LEVEL_TEMPLATE]
|
||||
|
||||
template_entity_ids = set()
|
||||
|
||||
if state_template is not None:
|
||||
temp_ids = state_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if level_template is not None:
|
||||
temp_ids = level_template.extract_entities()
|
||||
if str(temp_ids) != MATCH_ALL:
|
||||
template_entity_ids |= set(temp_ids)
|
||||
|
||||
if not template_entity_ids:
|
||||
template_entity_ids = MATCH_ALL
|
||||
|
||||
entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids)
|
||||
|
||||
lights.append(
|
||||
LightTemplate(
|
||||
hass, device, friendly_name, state_template,
|
||||
on_action, off_action, level_action, level_template,
|
||||
entity_ids)
|
||||
)
|
||||
|
||||
if not lights:
|
||||
_LOGGER.error("No lights added")
|
||||
return False
|
||||
|
||||
async_add_devices(lights, True)
|
||||
return True
|
||||
|
||||
|
||||
class LightTemplate(Light):
|
||||
"""Representation of a templated Light, including dimmable."""
|
||||
|
||||
def __init__(self, hass, device_id, friendly_name, state_template,
|
||||
on_action, off_action, level_action, level_template,
|
||||
entity_ids):
|
||||
"""Initialize the light."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||
self._name = friendly_name
|
||||
self._template = state_template
|
||||
self._on_script = Script(hass, on_action)
|
||||
self._off_script = Script(hass, off_action)
|
||||
self._level_script = None
|
||||
if level_action is not None:
|
||||
self._level_script = Script(hass, level_action)
|
||||
self._level_template = level_template
|
||||
|
||||
self._state = False
|
||||
self._brightness = None
|
||||
self._entities = entity_ids
|
||||
|
||||
if self._template is not None:
|
||||
self._template.hass = self.hass
|
||||
if self._level_template is not None:
|
||||
self._level_template.hass = self.hass
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of the light."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
if self._level_script is not None:
|
||||
return SUPPORT_BRIGHTNESS
|
||||
|
||||
return 0
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@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_light_state_listener(entity, old_state, new_state):
|
||||
"""Handle target device state changes."""
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
@callback
|
||||
def template_light_startup(event):
|
||||
"""Update template on startup."""
|
||||
if (self._template is not None or
|
||||
self._level_template is not None):
|
||||
async_track_state_change(
|
||||
self.hass, self._entities, template_light_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_light_startup)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
optimistic_set = False
|
||||
# set optimistic states
|
||||
if self._template is None:
|
||||
self._state = True
|
||||
optimistic_set = True
|
||||
|
||||
if self._level_template is None and ATTR_BRIGHTNESS in kwargs:
|
||||
_LOGGER.info("Optimistically setting brightness to %s",
|
||||
kwargs[ATTR_BRIGHTNESS])
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
optimistic_set = True
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs and self._level_script:
|
||||
self.hass.async_add_job(self._level_script.async_run(
|
||||
{"brightness": kwargs[ATTR_BRIGHTNESS]}))
|
||||
else:
|
||||
self.hass.async_add_job(self._on_script.async_run())
|
||||
|
||||
if optimistic_set:
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
self.hass.async_add_job(self._off_script.async_run())
|
||||
if self._template is None:
|
||||
self._state = False
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the state from the template."""
|
||||
if self._template is not None:
|
||||
try:
|
||||
state = self._template.async_render().lower()
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._state = None
|
||||
|
||||
if state in _VALID_STATES:
|
||||
self._state = state in ('true', STATE_ON)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid light is_on state: %s. ' +
|
||||
'Expected: %s',
|
||||
state, ', '.join(_VALID_STATES))
|
||||
self._state = None
|
||||
|
||||
if self._level_template is not None:
|
||||
try:
|
||||
brightness = self._level_template.async_render()
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._state = None
|
||||
|
||||
if 0 <= int(brightness) <= 255:
|
||||
self._brightness = brightness
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid brightness : %s' +
|
||||
'Expected: 0-255',
|
||||
brightness)
|
||||
self._brightness = None
|
||||
@@ -7,7 +7,8 @@ https://home-assistant.io/components/light.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ENTITY_ID_FORMAT,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light)
|
||||
from homeassistant.components.vera import (
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
@@ -15,8 +16,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
SUPPORT_VERA = SUPPORT_BRIGHTNESS
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -31,23 +30,34 @@ class VeraLight(VeraDevice, Light):
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the light."""
|
||||
self._state = False
|
||||
self._color = None
|
||||
self._brightness = None
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of the light."""
|
||||
if self.vera_device.is_dimmable:
|
||||
return self.vera_device.get_brightness()
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the color of the light."""
|
||||
return self._color
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_VERA
|
||||
if self._color:
|
||||
return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
|
||||
else:
|
||||
return SUPPORT_BRIGHTNESS
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable:
|
||||
if ATTR_RGB_COLOR in kwargs and self._color:
|
||||
self.vera_device.set_color(kwargs[ATTR_RGB_COLOR])
|
||||
elif ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable:
|
||||
self.vera_device.set_brightness(kwargs[ATTR_BRIGHTNESS])
|
||||
else:
|
||||
self.vera_device.switch_on()
|
||||
@@ -69,3 +79,8 @@ class VeraLight(VeraDevice, Light):
|
||||
def update(self):
|
||||
"""Call to update state."""
|
||||
self._state = self.vera_device.is_switched_on()
|
||||
if self.vera_device.is_dimmable:
|
||||
# If it is dimmable, both functions exist. In case color
|
||||
# is not supported, it will return None
|
||||
self._brightness = self.vera_device.get_brightness()
|
||||
self._color = self.vera_device.get_color()
|
||||
|
||||
@@ -19,8 +19,6 @@ DEPENDENCIES = ['wink']
|
||||
|
||||
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
|
||||
|
||||
RGB_MODES = ['hsb', 'rgb']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Wink lights."""
|
||||
@@ -62,8 +60,6 @@ class WinkLight(WinkDevice, Light):
|
||||
"""Define current bulb color in RGB."""
|
||||
if not self.wink.supports_hue_saturation():
|
||||
return None
|
||||
elif self.wink.color_model() not in RGB_MODES:
|
||||
return False
|
||||
else:
|
||||
hue = self.wink.color_hue()
|
||||
saturation = self.wink.color_saturation()
|
||||
|
||||
@@ -16,13 +16,13 @@ from homeassistant.util.color import (
|
||||
from homeassistant.const import CONF_DEVICES, CONF_NAME
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP,
|
||||
ATTR_FLASH, FLASH_SHORT, FLASH_LONG,
|
||||
ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
|
||||
SUPPORT_COLOR_TEMP, SUPPORT_FLASH,
|
||||
SUPPORT_COLOR_TEMP, SUPPORT_FLASH, SUPPORT_EFFECT,
|
||||
Light, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['yeelight==0.2.2']
|
||||
REQUIREMENTS = ['yeelight==0.3.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,8 +50,44 @@ SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS |
|
||||
|
||||
SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT |
|
||||
SUPPORT_RGB_COLOR |
|
||||
SUPPORT_EFFECT |
|
||||
SUPPORT_COLOR_TEMP)
|
||||
|
||||
EFFECT_DISCO = "Disco"
|
||||
EFFECT_TEMP = "Slow Temp"
|
||||
EFFECT_STROBE = "Strobe epilepsy!"
|
||||
EFFECT_STROBE_COLOR = "Strobe color"
|
||||
EFFECT_ALARM = "Alarm"
|
||||
EFFECT_POLICE = "Police"
|
||||
EFFECT_POLICE2 = "Police2"
|
||||
EFFECT_CHRISTMAS = "Christmas"
|
||||
EFFECT_RGB = "RGB"
|
||||
EFFECT_RANDOM_LOOP = "Random Loop"
|
||||
EFFECT_FAST_RANDOM_LOOP = "Fast Random Loop"
|
||||
EFFECT_SLOWDOWN = "Slowdown"
|
||||
EFFECT_WHATSAPP = "WhatsApp"
|
||||
EFFECT_FACEBOOK = "Facebook"
|
||||
EFFECT_TWITTER = "Twitter"
|
||||
EFFECT_STOP = "Stop"
|
||||
|
||||
YEELIGHT_EFFECT_LIST = [
|
||||
EFFECT_DISCO,
|
||||
EFFECT_TEMP,
|
||||
EFFECT_STROBE,
|
||||
EFFECT_STROBE_COLOR,
|
||||
EFFECT_ALARM,
|
||||
EFFECT_POLICE,
|
||||
EFFECT_POLICE2,
|
||||
EFFECT_CHRISTMAS,
|
||||
EFFECT_RGB,
|
||||
EFFECT_RANDOM_LOOP,
|
||||
EFFECT_FAST_RANDOM_LOOP,
|
||||
EFFECT_SLOWDOWN,
|
||||
EFFECT_WHATSAPP,
|
||||
EFFECT_FACEBOOK,
|
||||
EFFECT_TWITTER,
|
||||
EFFECT_STOP]
|
||||
|
||||
|
||||
def _cmd(func):
|
||||
"""Define a wrapper to catch exceptions from the bulb."""
|
||||
@@ -116,6 +152,11 @@ class YeelightLight(Light):
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
"""Return the list of supported effects."""
|
||||
return YEELIGHT_EFFECT_LIST
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the ID of this light."""
|
||||
@@ -286,6 +327,54 @@ class YeelightLight(Light):
|
||||
except BulbException as ex:
|
||||
_LOGGER.error("Unable to set flash: %s", ex)
|
||||
|
||||
@_cmd
|
||||
def set_effect(self, effect) -> None:
|
||||
"""Activate effect."""
|
||||
if effect:
|
||||
from yeelight import (Flow, BulbException)
|
||||
from yeelight.transitions import (disco, temp, strobe, pulse,
|
||||
strobe_color, alarm, police,
|
||||
police2, christmas, rgb,
|
||||
randomloop, slowdown)
|
||||
if effect == EFFECT_STOP:
|
||||
self._bulb.stop_flow()
|
||||
return
|
||||
if effect == EFFECT_DISCO:
|
||||
flow = Flow(count=0, transitions=disco())
|
||||
if effect == EFFECT_TEMP:
|
||||
flow = Flow(count=0, transitions=temp())
|
||||
if effect == EFFECT_STROBE:
|
||||
flow = Flow(count=0, transitions=strobe())
|
||||
if effect == EFFECT_STROBE_COLOR:
|
||||
flow = Flow(count=0, transitions=strobe_color())
|
||||
if effect == EFFECT_ALARM:
|
||||
flow = Flow(count=0, transitions=alarm())
|
||||
if effect == EFFECT_POLICE:
|
||||
flow = Flow(count=0, transitions=police())
|
||||
if effect == EFFECT_POLICE2:
|
||||
flow = Flow(count=0, transitions=police2())
|
||||
if effect == EFFECT_CHRISTMAS:
|
||||
flow = Flow(count=0, transitions=christmas())
|
||||
if effect == EFFECT_RGB:
|
||||
flow = Flow(count=0, transitions=rgb())
|
||||
if effect == EFFECT_RANDOM_LOOP:
|
||||
flow = Flow(count=0, transitions=randomloop())
|
||||
if effect == EFFECT_FAST_RANDOM_LOOP:
|
||||
flow = Flow(count=0, transitions=randomloop(duration=250))
|
||||
if effect == EFFECT_SLOWDOWN:
|
||||
flow = Flow(count=0, transitions=slowdown())
|
||||
if effect == EFFECT_WHATSAPP:
|
||||
flow = Flow(count=2, transitions=pulse(37, 211, 102))
|
||||
if effect == EFFECT_FACEBOOK:
|
||||
flow = Flow(count=2, transitions=pulse(59, 89, 152))
|
||||
if effect == EFFECT_TWITTER:
|
||||
flow = Flow(count=2, transitions=pulse(0, 172, 237))
|
||||
|
||||
try:
|
||||
self._bulb.start_flow(flow)
|
||||
except BulbException as ex:
|
||||
_LOGGER.error("Unable to set effect: %s", ex)
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the bulb on."""
|
||||
import yeelight
|
||||
@@ -293,6 +382,7 @@ class YeelightLight(Light):
|
||||
colortemp = kwargs.get(ATTR_COLOR_TEMP)
|
||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
|
||||
duration = int(self.config[CONF_TRANSITION]) # in ms
|
||||
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
|
||||
@@ -317,6 +407,7 @@ class YeelightLight(Light):
|
||||
self.set_colortemp(colortemp, duration)
|
||||
self.set_brightness(brightness, duration)
|
||||
self.set_flash(flash)
|
||||
self.set_effect(effect)
|
||||
except yeelight.BulbException as ex:
|
||||
_LOGGER.error("Unable to set bulb properties: %s", ex)
|
||||
return
|
||||
|
||||
@@ -47,11 +47,11 @@ TEMP_COLD_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 + TEMP_COLOR_MIN
|
||||
|
||||
def get_device(node, values, node_config, **kwargs):
|
||||
"""Create Z-Wave entity device."""
|
||||
name = '{}.{}'.format(DOMAIN, zwave.object_id(values.primary))
|
||||
refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
|
||||
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
|
||||
_LOGGER.debug("name=%s node_config=%s CONF_REFRESH_VALUE=%s"
|
||||
" CONF_REFRESH_DELAY=%s", name, node_config, refresh, delay)
|
||||
_LOGGER.debug("node=%d value=%d node_config=%s CONF_REFRESH_VALUE=%s"
|
||||
" CONF_REFRESH_DELAY=%s", node.node_id,
|
||||
values.primary.value_id, node_config, refresh, delay)
|
||||
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
|
||||
return ZwaveColorLight(values, refresh, delay)
|
||||
|
||||
@@ -25,6 +25,8 @@ from homeassistant.components import group
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
|
||||
DOMAIN = 'lock'
|
||||
DEPENDENCIES = ['group']
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks')
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@@ -33,8 +35,6 @@ GROUP_NAME_ALL_LOCKS = 'all locks'
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
LOCK_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_CODE): cv.string,
|
||||
@@ -108,8 +108,8 @@ def async_setup(hass, config):
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
@@ -150,8 +150,7 @@ class LockDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.lock, **kwargs))
|
||||
return self.hass.async_add_job(ft.partial(self.lock, **kwargs))
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the lock."""
|
||||
@@ -162,8 +161,7 @@ class LockDevice(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.unlock, **kwargs))
|
||||
return self.hass.async_add_job(ft.partial(self.unlock, **kwargs))
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Support for Sesame, by CANDY HOUSE.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/lock.sesame/
|
||||
"""
|
||||
from typing import Callable # noqa
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, CONF_EMAIL, CONF_PASSWORD,
|
||||
STATE_LOCKED, STATE_UNLOCKED)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
REQUIREMENTS = ['pysesame==0.1.0']
|
||||
|
||||
ATTR_DEVICE_ID = 'device_id'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_EMAIL): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the Sesame platform."""
|
||||
import pysesame
|
||||
|
||||
email = config.get(CONF_EMAIL)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
add_devices([SesameDevice(sesame) for
|
||||
sesame in pysesame.get_sesames(email, password)])
|
||||
|
||||
|
||||
class SesameDevice(LockDevice):
|
||||
"""Representation of a Sesame device."""
|
||||
|
||||
_sesame = None
|
||||
|
||||
def __init__(self, sesame: object) -> None:
|
||||
"""Initialize the Sesame device."""
|
||||
self._sesame = sesame
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device."""
|
||||
return self._sesame.nickname
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._sesame.api_enabled
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool:
|
||||
"""Return True if the device is currently locked, else False."""
|
||||
return not self._sesame.is_unlocked
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the device."""
|
||||
if self._sesame.is_unlocked:
|
||||
return STATE_UNLOCKED
|
||||
return STATE_LOCKED
|
||||
|
||||
def lock(self, **kwargs) -> None:
|
||||
"""Lock the device."""
|
||||
self._sesame.lock()
|
||||
|
||||
def unlock(self, **kwargs) -> None:
|
||||
"""Unlock the device."""
|
||||
self._sesame.unlock()
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the internal state of the device."""
|
||||
self._sesame.update_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> dict:
|
||||
"""Return the state attributes."""
|
||||
attributes = {}
|
||||
attributes[ATTR_DEVICE_ID] = self._sesame.device_id
|
||||
attributes[ATTR_BATTERY_LEVEL] = self._sesame.battery
|
||||
return attributes
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user