Compare commits
285 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e13fd05e7d | |||
| a760673ad6 | |||
| 12dec93565 | |||
| c376bc2e45 | |||
| f0e5f68865 | |||
| 56f4486e0b | |||
| 828c469ef7 | |||
| 0a6d519b9d | |||
| 0c97fe7eac | |||
| e8ce41874c | |||
| 0ab0e35d59 | |||
| 51108b8fe9 | |||
| 9e6817b6d0 | |||
| 74581b57f8 | |||
| 4fcaea23a8 | |||
| b59c29943b | |||
| 1e8c00ac02 | |||
| 9d5c61b2f0 | |||
| f5eeb252a7 | |||
| 3b4ea864a1 | |||
| 3318f02664 | |||
| 438edc5ca1 | |||
| abcfcdd887 | |||
| fff269e790 | |||
| 81a27e726c | |||
| 7c120748ce | |||
| e83816c055 | |||
| cd2703e121 | |||
| c2828bac2c | |||
| ad7370e1c2 | |||
| 3b7f16f189 | |||
| cc03f7ee6a | |||
| ecc1429453 | |||
| 98568b5eb7 | |||
| 9d9ca64f26 | |||
| 1d31137616 | |||
| f86bd15580 | |||
| cbf65220aa | |||
| c100b8cb52 | |||
| 654ad41464 | |||
| a2abb4ae0a | |||
| 36e266442f | |||
| f3d9086ff4 | |||
| 0c09cfc6c4 | |||
| b0b6026c68 | |||
| 8f47a9109c | |||
| f0293eeac2 | |||
| e4317a6741 | |||
| 4b449f5f93 | |||
| 8760dc9b29 | |||
| 1831a7da68 | |||
| 3e34f34f6b | |||
| 3fec2955a5 | |||
| 2cf9254a08 | |||
| 333da0dc6d | |||
| 7b10f0a14f | |||
| fb6bdfaba9 | |||
| d7da90ae54 | |||
| a5bfcceacd | |||
| 4961ece931 | |||
| 7d99d6aad9 | |||
| 6dc93c2751 | |||
| 5c39eebea8 | |||
| ffd295b38b | |||
| 5d810dae86 | |||
| 486bcc4cae | |||
| cc2de5e1dc | |||
| 77d8e393a1 | |||
| c6bf529d38 | |||
| dac9716cf4 | |||
| 9043895407 | |||
| 2f08a91fdd | |||
| 1807b45222 | |||
| b4f392b181 | |||
| 8e8ec7a7c3 | |||
| 7edf14e55f | |||
| 7bea69ce83 | |||
| 8d31c5fbf6 | |||
| dc42b6358a | |||
| 06ceadfd54 | |||
| 4359e0babf | |||
| ee153062ab | |||
| fada6d3f49 | |||
| f6a5e0887d | |||
| 4f8d2ec317 | |||
| e63a96cf53 | |||
| a5c0831dc1 | |||
| 718949481f | |||
| 90639d33ab | |||
| 966809c1a1 | |||
| bc27d173d0 | |||
| fde291f866 | |||
| 49c399c358 | |||
| 8d1999dc12 | |||
| ee05a4ab89 | |||
| 8a42e1551a | |||
| 9cc3e7e47b | |||
| 54755df9ea | |||
| 84ebcd8a59 | |||
| f1280d3edb | |||
| c27074e6f7 | |||
| c8bfcd2ed4 | |||
| 42699b7a60 | |||
| 6bc07298d3 | |||
| 4ece4bf241 | |||
| 1a86fa5a02 | |||
| d54a634f11 | |||
| 5e1ff20b09 | |||
| 29266213a0 | |||
| 2aa89cfe07 | |||
| 879c816f5c | |||
| 4ae11c009d | |||
| dcd6f7a29e | |||
| fde4a7d029 | |||
| b83ff739bc | |||
| 8c9b3898fc | |||
| 95e0027924 | |||
| c67c20f752 | |||
| 1a1571cd52 | |||
| cca0d3ed44 | |||
| f0479855bd | |||
| 40aafcdf5d | |||
| 8c9557401f | |||
| ffd3081743 | |||
| d0275c8075 | |||
| f6c3832e90 | |||
| d29bdddaa7 | |||
| d3be056d15 | |||
| bffa0d2b04 | |||
| 23b65bfb30 | |||
| 1d4a7f1160 | |||
| dc08852fc2 | |||
| 3377f30613 | |||
| 84ca4d2a21 | |||
| 1366c93c83 | |||
| e5e2a151aa | |||
| bd1e533409 | |||
| 21e82bd037 | |||
| af9a0e8fea | |||
| abc5c3e128 | |||
| 543e8bb62e | |||
| 6ca828fd14 | |||
| 87b83f3602 | |||
| 5829cdfdf1 | |||
| d473f3407b | |||
| 9373d5e901 | |||
| d8abef9210 | |||
| 4fde0ffe9c | |||
| ba019c799a | |||
| 5581c6295e | |||
| 192db5bec3 | |||
| b8eaec565a | |||
| e0f35c0279 | |||
| 2eeeb9075a | |||
| 71ee290bfd | |||
| 7aad93e90d | |||
| a65f22378e | |||
| bb9db28c95 | |||
| d10f017441 | |||
| b6e0286d71 | |||
| 4451d2e847 | |||
| 229000b834 | |||
| 9704057959 | |||
| effb9e9d23 | |||
| effbb3bd4c | |||
| 471501d386 | |||
| ef94b5c77a | |||
| 60dcc9a5c0 | |||
| 5b4862cc3c | |||
| fbf945c18b | |||
| 609c25691a | |||
| 6e77877743 | |||
| 7b105a2150 | |||
| ee57a823af | |||
| 04b1621b65 | |||
| f5e24cb0bb | |||
| ac72dea09a | |||
| 2f474a0ed8 | |||
| 7a4cc8e082 | |||
| 92dc76773a | |||
| fe4abc8454 | |||
| 821d01f82c | |||
| b453834b2f | |||
| 97f14015ea | |||
| 4fb25cf16d | |||
| e7b5c5812c | |||
| 2ac423bd9d | |||
| ec7ca9a560 | |||
| cb298123d4 | |||
| c5bf4fe339 | |||
| 57c5ed33ee | |||
| 3be0103259 | |||
| 614b5da170 | |||
| acf6d4ab82 | |||
| d3acb25070 | |||
| 222ad3ab6d | |||
| 5ae2bcdbb7 | |||
| 6c9742afc4 | |||
| cf924cd14d | |||
| f2267437df | |||
| 233920f22c | |||
| 7536e825fa | |||
| e12a9eaadd | |||
| fb184b4b6f | |||
| 63ff173305 | |||
| 903e6b5aee | |||
| 46ce26eb7a | |||
| b1bba3675d | |||
| ed5d10448e | |||
| 652c006cbc | |||
| b67c5df525 | |||
| a7d5a8d93e | |||
| c48c2b00a8 | |||
| 9bc5cd2d4b | |||
| ecf3a9cb36 | |||
| 074e31bcf9 | |||
| 63cc658010 | |||
| 8682f21fc5 | |||
| aa28e6727d | |||
| 12129f0e6a | |||
| 8a7cfce67b | |||
| 5e71e9b826 | |||
| db8bb53984 | |||
| 692f4c293b | |||
| da37380410 | |||
| fa4aa2244e | |||
| c63bdd5afe | |||
| 20a9899354 | |||
| fe6a4b8ae5 | |||
| 143044f8f1 | |||
| d655c0e358 | |||
| 46e030662d | |||
| 5779d64e98 | |||
| 83a5f932d1 | |||
| a12fa2e5bf | |||
| ee37fc344b | |||
| 8cc0748db3 | |||
| 0ecceb601b | |||
| 2a1a5e53a1 | |||
| c8b782189e | |||
| 74016c4179 | |||
| c30c8df449 | |||
| 58de661ad5 | |||
| f4a97db783 | |||
| b220ceec9c | |||
| fb796b5481 | |||
| ea5bec3ef4 | |||
| 8185587100 | |||
| 061a38cc3b | |||
| 23fc5e2c9f | |||
| 6496c38ce6 | |||
| 3363b88a73 | |||
| 2e17d0926a | |||
| 85ac50cc77 | |||
| da61b18392 | |||
| 8a88af20da | |||
| f8527e9773 | |||
| 7977996c0d | |||
| 22681fbe08 | |||
| 1e655eea74 | |||
| 8d940fb585 | |||
| afe3dd8dbb | |||
| bf96f28e95 | |||
| 5cba3085b4 | |||
| 407a419c83 | |||
| 4ab778fd97 | |||
| ee7d4710c4 | |||
| 3a6434f566 | |||
| a2f5b630d6 | |||
| 3f2fa0ed5a | |||
| 865865ca0f | |||
| 05ced33648 | |||
| b4165fe9f3 | |||
| 47aa8c387a | |||
| 2b94857ffd | |||
| 0bf5021c2c | |||
| b82003ae08 | |||
| c13fdd23c1 | |||
| e6e0e5263a | |||
| 0981956caa | |||
| e077998d38 | |||
| 7123ec14be | |||
| 8e4394f173 | |||
| 5e56bc7464 | |||
| ed20f7e359 |
+27
-5
@@ -11,9 +11,15 @@ omit =
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
homeassistant/components/amcrest.py
|
||||
homeassistant/components/*/amcrest.py
|
||||
|
||||
homeassistant/components/apcupsd.py
|
||||
homeassistant/components/*/apcupsd.py
|
||||
|
||||
homeassistant/components/apple_tv.py
|
||||
homeassistant/components/*/apple_tv.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
@@ -89,6 +95,9 @@ omit =
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/lametric.py
|
||||
homeassistant/components/*/lametric.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
@@ -163,6 +172,12 @@ omit =
|
||||
homeassistant/components/twilio.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
homeassistant/components/velbus.py
|
||||
homeassistant/components/*/velbus.py
|
||||
|
||||
homeassistant/components/velux.py
|
||||
homeassistant/components/*/velux.py
|
||||
|
||||
homeassistant/components/vera.py
|
||||
homeassistant/components/*/vera.py
|
||||
@@ -181,6 +196,9 @@ omit =
|
||||
homeassistant/components/wink.py
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/xiaomi.py
|
||||
homeassistant/components/*/xiaomi.py
|
||||
|
||||
homeassistant/components/zabbix.py
|
||||
homeassistant/components/*/zabbix.py
|
||||
|
||||
@@ -196,6 +214,7 @@ omit =
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||
@@ -211,7 +230,6 @@ omit =
|
||||
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
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
@@ -263,7 +281,6 @@ omit =
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/device_tracker/xiaomi.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
@@ -292,6 +309,7 @@ omit =
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/sensehat.py
|
||||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/tplink.py
|
||||
homeassistant/components/light/tradfri.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
@@ -301,8 +319,8 @@ omit =
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/media_extractor.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/apple_tv.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
@@ -339,6 +357,7 @@ omit =
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vizio.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
@@ -375,11 +394,11 @@ omit =
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/prometheus.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/amcrest.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
@@ -390,7 +409,7 @@ omit =
|
||||
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/citybikes.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
@@ -404,6 +423,7 @@ omit =
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
@@ -450,6 +470,7 @@ omit =
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/opensky.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/otp.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
@@ -511,6 +532,7 @@ omit =
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/switch/xiaomi_vacuum.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@ matrix:
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.6-dev"
|
||||
env: TOXENV=py36
|
||||
# - python: "3.6-dev"
|
||||
# env: TOXENV=py36
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
# allow_failures:
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# People marked here will be automatically requested for a review
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
setup.py @home-assistant/core
|
||||
homeassistant/*.py @home-assistant/core
|
||||
homeassistant/helpers/* @home-assistant/core
|
||||
homeassistant/util/* @home-assistant/core
|
||||
homeassistant/components/api.py @home-assistant/core
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/configurator.py @home-assistant/core
|
||||
homeassistant/components/group.py @home-assistant/core
|
||||
homeassistant/components/history.py @home-assistant/core
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/input_*.py @home-assistant/core
|
||||
homeassistant/components/introduction.py @home-assistant/core
|
||||
homeassistant/components/logger.py @home-assistant/core
|
||||
homeassistant/components/mqtt/* @home-assistant/core
|
||||
homeassistant/components/panel_custom.py @home-assistant/core
|
||||
homeassistant/components/panel_iframe.py @home-assistant/core
|
||||
homeassistant/components/persistent_notification.py @home-assistant/core
|
||||
homeassistant/components/scene/__init__.py @home-assistant/core
|
||||
homeassistant/components/scene/hass.py @home-assistant/core
|
||||
homeassistant/components/script.py @home-assistant/core
|
||||
homeassistant/components/shell_command.py @home-assistant/core
|
||||
homeassistant/components/sun.py @home-assistant/core
|
||||
homeassistant/components/updater.py @home-assistant/core
|
||||
homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/websocket_api.py @home-assistant/core
|
||||
homeassistant/components/zone.py @home-assistant/core
|
||||
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
|
||||
# Indiviudal components
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/media_player/kodi.py @armills
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs|
|
||||
==============================================================================================================================================================================================
|
||||
Home Assistant |Build Status| |Coverage Status| |Chat Status|
|
||||
=============================================================
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
|
||||
|
||||
@@ -31,6 +31,8 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
|
||||
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
|
||||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
:target: https://discord.gg/c5DvZ4e
|
||||
.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://img.shields.io/badge/gitter-general-blue.svg
|
||||
:target: https://gitter.im/home-assistant/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| image:: https://img.shields.io/badge/gitter-development-yellowgreen.svg
|
||||
|
||||
@@ -229,8 +229,8 @@ def cmdline() -> List[str]:
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
arg != '--daemon']
|
||||
else:
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
|
||||
+28
-14
@@ -19,6 +19,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.package import async_get_user_site, get_user_site
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
@@ -39,7 +40,7 @@ def from_config_dict(config: Dict[str, Any],
|
||||
skip_pip: bool=False,
|
||||
log_rotate_days: Any=None) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a config dict.
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
"""
|
||||
@@ -48,7 +49,8 @@ def from_config_dict(config: Dict[str, Any],
|
||||
if config_dir is not None:
|
||||
config_dir = os.path.abspath(config_dir)
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
hass.loop.run_until_complete(
|
||||
async_mount_local_lib_path(config_dir, hass.loop))
|
||||
|
||||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
@@ -69,7 +71,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
skip_pip: bool=False,
|
||||
log_rotate_days: Any=None) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a config dict.
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
@@ -90,8 +92,8 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
_LOGGER.warning('Skipping pip installation of required modules. '
|
||||
'This may cause issues.')
|
||||
_LOGGER.warning("Skipping pip installation of required modules. "
|
||||
"This may cause issues")
|
||||
|
||||
if not loader.PREPARED:
|
||||
yield from hass.async_add_job(loader.prepare, hass)
|
||||
@@ -116,13 +118,13 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
# pylint: disable=not-an-iterable
|
||||
res = yield from core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"further initialization aborted")
|
||||
return hass
|
||||
|
||||
yield from persistent_notification.async_setup(hass, config)
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
_LOGGER.info("Home Assistant core initialized")
|
||||
|
||||
# stage 1
|
||||
for component in components:
|
||||
@@ -141,7 +143,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
stop = time()
|
||||
_LOGGER.info('Home Assistant initialized in %.2fs', stop-start)
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
||||
|
||||
async_register_signal_handling(hass)
|
||||
return hass
|
||||
@@ -183,7 +185,7 @@ 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.async_add_job(mount_local_lib_path, config_dir)
|
||||
yield from async_mount_local_lib_path(config_dir, hass.loop)
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
@@ -191,7 +193,7 @@ def async_from_config_file(config_path: str,
|
||||
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)
|
||||
_LOGGER.error("Error loading %s: %s", config_path, err)
|
||||
return None
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
@@ -276,11 +278,23 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path."""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
lib_dir = get_user_site(deps_dir)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_mount_local_lib_path(config_dir: str,
|
||||
loop: asyncio.AbstractEventLoop) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
Async friendly.
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
if deps_dir not in sys.path:
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
lib_dir = yield from async_get_user_site(deps_dir, loop=loop)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
@@ -15,7 +15,6 @@ import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
||||
@@ -33,25 +32,27 @@ def is_on(hass, entity_id=None):
|
||||
If there is no entity id given we will check all.
|
||||
"""
|
||||
if entity_id:
|
||||
group = get_component('group')
|
||||
|
||||
entity_ids = group.expand_entity_ids(hass, [entity_id])
|
||||
entity_ids = hass.components.group.expand_entity_ids([entity_id])
|
||||
else:
|
||||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = ha.split_entity_id(entity_id)[0]
|
||||
|
||||
module = get_component(domain)
|
||||
for ent_id in entity_ids:
|
||||
domain = ha.split_entity_id(ent_id)[0]
|
||||
|
||||
try:
|
||||
if module.is_on(hass, entity_id):
|
||||
return True
|
||||
component = getattr(hass.components, domain)
|
||||
|
||||
except AttributeError:
|
||||
# module is None or method is_on does not exist
|
||||
_LOGGER.exception("Failed to call %s.is_on for %s",
|
||||
module, entity_id)
|
||||
except ImportError:
|
||||
_LOGGER.error('Failed to call %s.is_on: component not found',
|
||||
domain)
|
||||
continue
|
||||
|
||||
if not hasattr(component, 'is_on'):
|
||||
_LOGGER.warning("Component %s has no is_on method.", domain)
|
||||
continue
|
||||
|
||||
if component.is_on(ent_id):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -161,10 +162,9 @@ def async_setup(hass, config):
|
||||
return
|
||||
|
||||
if errors:
|
||||
notif = get_component('persistent_notification')
|
||||
_LOGGER.error(errors)
|
||||
notif.async_create(
|
||||
hass, "Config error. See dev-info panel for details.",
|
||||
hass.components.persistent_notification.async_create(
|
||||
"Config error. See dev-info panel for details.",
|
||||
"Config validating", "{0}.check_config".format(ha.DOMAIN))
|
||||
return
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -44,6 +45,7 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_disarm(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for disarm."""
|
||||
data = {}
|
||||
@@ -55,6 +57,7 @@ def alarm_disarm(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_home(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm home."""
|
||||
data = {}
|
||||
@@ -66,6 +69,7 @@ def alarm_arm_home(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_arm_away(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for arm away."""
|
||||
data = {}
|
||||
@@ -77,6 +81,7 @@ def alarm_arm_away(hass, code=None, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def alarm_trigger(hass, code=None, entity_id=None):
|
||||
"""Send the alarm the command for trigger."""
|
||||
data = {}
|
||||
|
||||
@@ -92,8 +92,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
|
||||
@@ -113,8 +113,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
else:
|
||||
return '^\\d{4,6}$'
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -99,8 +99,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return self._pre_trigger_state
|
||||
return self._pre_trigger_state
|
||||
|
||||
return self._state
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
Support for manual alarms controllable via MQTT.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.manual_mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM,
|
||||
CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.core import callback
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
|
||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
||||
DEFAULT_DISARM = 'DISARM'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PLATFORM): 'manual_mqtt',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the manual MQTT alarm platform."""
|
||||
add_devices([ManualMQTTAlarm(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
|
||||
config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config.get(mqtt.CONF_STATE_TOPIC),
|
||||
config.get(mqtt.CONF_COMMAND_TOPIC),
|
||||
config.get(mqtt.CONF_QOS),
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY))])
|
||||
|
||||
|
||||
class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
"""
|
||||
Representation of an alarm status.
|
||||
|
||||
When armed, will be pending for 'pending_time', after that armed.
|
||||
When triggered, will be pending for 'trigger_time'. After that will be
|
||||
triggered for 'trigger_time', after that we return to the previous state
|
||||
or disarm if `disarm_after_trigger` is true.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, pending_time,
|
||||
trigger_time, disarm_after_trigger,
|
||||
state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away):
|
||||
"""Init the manual MQTT alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._pending_time = datetime.timedelta(seconds=pending_time)
|
||||
self._trigger_time = datetime.timedelta(seconds=trigger_time)
|
||||
self._disarm_after_trigger = disarm_after_trigger
|
||||
self._pre_trigger_state = self._state
|
||||
self._state_ts = None
|
||||
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
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."""
|
||||
if self._state in (STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY) and \
|
||||
self._pending_time and self._state_ts + self._pending_time > \
|
||||
dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
|
||||
if self._state_ts + self._pending_time > dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time +
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
return self._pre_trigger_state
|
||||
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, STATE_ALARM_DISARMED):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_HOME):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, STATE_ALARM_ARMED_AWAY):
|
||||
return
|
||||
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._trigger_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time + self._trigger_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
async_track_state_change(
|
||||
self.hass, self.entity_id, self._async_state_changed_listener
|
||||
)
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Run when new MQTT message has been received."""
|
||||
if payload == self._payload_disarm:
|
||||
self.async_alarm_disarm(self._code)
|
||||
elif payload == self._payload_arm_home:
|
||||
self.async_alarm_arm_home(self._code)
|
||||
elif payload == self._payload_arm_away:
|
||||
self.async_alarm_arm_away(self._code)
|
||||
else:
|
||||
_LOGGER.warning("Received unexpected payload: %s", payload)
|
||||
return
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._command_topic, message_received, self._qos)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
||||
"""Publish state change to MQTT."""
|
||||
mqtt.async_publish(self.hass, self._state_topic, new_state.state,
|
||||
self._qos, True)
|
||||
@@ -15,9 +15,8 @@ from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==1.0.2']
|
||||
REQUIREMENTS = ['simplisafe-python==1.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
simplisafe = SimpliSafeApiInterface()
|
||||
status = simplisafe.set_credentials(username, password)
|
||||
if status:
|
||||
@@ -53,8 +51,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
else:
|
||||
message = 'Failed to log into SimpliSafe. Check credentials.'
|
||||
_LOGGER.error(message)
|
||||
persistent_notification.create(
|
||||
hass, message,
|
||||
hass.components.persistent_notification.create(
|
||||
message,
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
@@ -80,8 +78,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN,
|
||||
CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.7']
|
||||
REQUIREMENTS = ['total_connect_client==0.11']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -39,10 +39,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
"""Representation a Wink camera alarm."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink alarm."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['alarmdecoder==0.12.1.0']
|
||||
REQUIREMENTS = ['alarmdecoder==0.12.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -271,14 +271,14 @@ class Alert(ToggleEntity):
|
||||
'notify', target, {'message': self._done_message})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self):
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Async Unacknowledge alert."""
|
||||
_LOGGER.debug("Reset Alert: %s", self._name)
|
||||
self._ack = False
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self):
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Async Acknowledge alert."""
|
||||
_LOGGER.debug("Acknowledged Alert: %s", self._name)
|
||||
self._ack = True
|
||||
|
||||
@@ -15,8 +15,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.helpers import intent, template, config_validation as cv
|
||||
from homeassistant.components import http
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -60,6 +60,12 @@ class SpeechType(enum.Enum):
|
||||
ssml = "SSML"
|
||||
|
||||
|
||||
SPEECH_MAPPINGS = {
|
||||
'plain': SpeechType.plaintext,
|
||||
'ssml': SpeechType.ssml,
|
||||
}
|
||||
|
||||
|
||||
class CardType(enum.Enum):
|
||||
"""The Alexa card types."""
|
||||
|
||||
@@ -69,20 +75,6 @@ class CardType(enum.Enum):
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
CONF_INTENTS: {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_CARD): {
|
||||
vol.Required(CONF_TYPE): cv.enum(CardType),
|
||||
vol.Required(CONF_TITLE): cv.template,
|
||||
vol.Required(CONF_CONTENT): cv.template,
|
||||
},
|
||||
vol.Optional(CONF_SPEECH): {
|
||||
vol.Required(CONF_TYPE): cv.enum(SpeechType),
|
||||
vol.Required(CONF_TEXT): cv.template,
|
||||
}
|
||||
}
|
||||
},
|
||||
CONF_FLASH_BRIEFINGS: {
|
||||
cv.string: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
|
||||
@@ -96,40 +88,27 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
|
||||
|
||||
hass.http.register_view(AlexaIntentsView(hass, intents))
|
||||
hass.http.register_view(AlexaIntentsView)
|
||||
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlexaIntentsView(HomeAssistantView):
|
||||
class AlexaIntentsView(http.HomeAssistantView):
|
||||
"""Handle Alexa requests."""
|
||||
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:alexa'
|
||||
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__()
|
||||
|
||||
intents = copy.deepcopy(intents)
|
||||
template.attach(hass, intents)
|
||||
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(
|
||||
hass, intent[CONF_ACTION], "Alexa intent {}".format(name))
|
||||
|
||||
self.intents = intents
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
hass = request.app['hass']
|
||||
data = yield from request.json()
|
||||
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
@@ -146,14 +125,14 @@ class AlexaIntentsView(HomeAssistantView):
|
||||
if req_type == 'SessionEndedRequest':
|
||||
return None
|
||||
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(request.app['hass'], intent)
|
||||
alexa_intent_info = req.get('intent')
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
|
||||
if req_type == 'LaunchRequest':
|
||||
response.add_speech(
|
||||
alexa_response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"Hello, and welcome to the future. How may I help?")
|
||||
return self.json(response)
|
||||
return self.json(alexa_response)
|
||||
|
||||
if req_type != 'IntentRequest':
|
||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
||||
@@ -161,38 +140,47 @@ class AlexaIntentsView(HomeAssistantView):
|
||||
'Received unsupported request: {}'.format(req_type),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
intent_name = intent['name']
|
||||
config = self.intents.get(intent_name)
|
||||
intent_name = alexa_intent_info['name']
|
||||
|
||||
if config is None:
|
||||
try:
|
||||
intent_response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_name,
|
||||
{key: {'value': value} for key, value
|
||||
in alexa_response.variables.items()})
|
||||
except intent.UnknownIntent as err:
|
||||
_LOGGER.warning('Received unknown intent %s', intent_name)
|
||||
response.add_speech(
|
||||
alexa_response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"This intent is not yet configured within Home Assistant.")
|
||||
return self.json(response)
|
||||
return self.json(alexa_response)
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
except intent.InvalidSlotInfo as err:
|
||||
_LOGGER.error('Received invalid slot data from Alexa: %s', err)
|
||||
return self.json_message('Invalid slot data received',
|
||||
HTTP_BAD_REQUEST)
|
||||
except intent.IntentError:
|
||||
_LOGGER.exception('Error handling request for %s', intent_name)
|
||||
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
|
||||
|
||||
if action is not None:
|
||||
yield from action.async_run(response.variables)
|
||||
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
||||
if intent_speech in intent_response.speech:
|
||||
alexa_response.add_speech(
|
||||
alexa_speech,
|
||||
intent_response.speech[intent_speech]['speech'])
|
||||
break
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(speech[CONF_TYPE], speech[CONF_TEXT])
|
||||
if 'simple' in intent_response.card:
|
||||
alexa_response.add_card(
|
||||
'simple', intent_response.card['simple']['title'],
|
||||
intent_response.card['simple']['content'])
|
||||
|
||||
if card is not None:
|
||||
response.add_card(card[CONF_TYPE], card[CONF_TITLE],
|
||||
card[CONF_CONTENT])
|
||||
|
||||
return self.json(response)
|
||||
return self.json(alexa_response)
|
||||
|
||||
|
||||
class AlexaResponse(object):
|
||||
"""Help generating the response for Alexa."""
|
||||
|
||||
def __init__(self, hass, intent=None):
|
||||
def __init__(self, hass, intent_info):
|
||||
"""Initialize the response."""
|
||||
self.hass = hass
|
||||
self.speech = None
|
||||
@@ -201,8 +189,9 @@ class AlexaResponse(object):
|
||||
self.session_attributes = {}
|
||||
self.should_end_session = True
|
||||
self.variables = {}
|
||||
if intent is not None and 'slots' in intent:
|
||||
for key, value in intent['slots'].items():
|
||||
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
||||
if intent_info is not None:
|
||||
for key, value in intent_info.get('slots', {}).items():
|
||||
if 'value' in value:
|
||||
underscored_key = key.replace('.', '_')
|
||||
self.variables[underscored_key] = value['value']
|
||||
@@ -272,7 +261,7 @@ class AlexaResponse(object):
|
||||
}
|
||||
|
||||
|
||||
class AlexaFlashBriefingView(HomeAssistantView):
|
||||
class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
"""Handle Alexa Flash Briefing skill requests."""
|
||||
|
||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
This component provides basic support for Amcrest IP cameras.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/amcrest/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.1']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
DOMAIN = 'amcrest'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
AUTHENTICATION_LIST = {
|
||||
'basic': 'basic'
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSORS = {
|
||||
'motion_detector': ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera
|
||||
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
try:
|
||||
camera.current_time
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||
name = device.get(CONF_NAME)
|
||||
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
|
||||
|
||||
username = device.get(CONF_USERNAME)
|
||||
password = device.get(CONF_PASSWORD)
|
||||
|
||||
# currently aiohttp only works with basic authentication
|
||||
# only valid for mjpeg streaming
|
||||
if username is not None and password is not None:
|
||||
if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION:
|
||||
authentication = aiohttp.BasicAuth(username, password)
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_AUTHENTICATION: authentication,
|
||||
CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments,
|
||||
CONF_NAME: name,
|
||||
CONF_RESOLUTION: resolution,
|
||||
CONF_STREAM_SOURCE: stream_source,
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
return True
|
||||
@@ -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.10']
|
||||
REQUIREMENTS = ['apcaccess==0.0.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -198,8 +198,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
@@ -213,7 +212,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
|
||||
new_state = data.get('state')
|
||||
|
||||
if not new_state:
|
||||
if new_state is None:
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = data.get('attributes')
|
||||
@@ -237,8 +236,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
"""Remove entity."""
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
|
||||
@@ -5,13 +5,12 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/apiai/
|
||||
"""
|
||||
import asyncio
|
||||
import copy
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import PROJECT_NAME, HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.helpers import intent, template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -29,24 +28,14 @@ DOMAIN = 'apiai'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
CONF_INTENTS: {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_SPEECH): cv.template,
|
||||
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ASYNC_ACTION,
|
||||
default=DEFAULT_CONF_ASYNC_ACTION): cv.boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
DOMAIN: {}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Activate API.AI component."""
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
|
||||
hass.http.register_view(ApiaiIntentsView(hass, intents))
|
||||
hass.http.register_view(ApiaiIntentsView)
|
||||
|
||||
return True
|
||||
|
||||
@@ -57,24 +46,10 @@ class ApiaiIntentsView(HomeAssistantView):
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:apiai'
|
||||
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize API.AI view."""
|
||||
super().__init__()
|
||||
|
||||
self.hass = hass
|
||||
intents = copy.deepcopy(intents)
|
||||
template.attach(hass, intents)
|
||||
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(
|
||||
hass, intent[CONF_ACTION], "Apiai intent {}".format(name))
|
||||
|
||||
self.intents = intents
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle API.AI."""
|
||||
hass = request.app['hass']
|
||||
data = yield from request.json()
|
||||
|
||||
_LOGGER.debug("Received api.ai request: %s", data)
|
||||
@@ -91,55 +66,41 @@ class ApiaiIntentsView(HomeAssistantView):
|
||||
if action_incomplete:
|
||||
return None
|
||||
|
||||
# use intent to no mix HASS actions with this parameter
|
||||
intent = req.get('action')
|
||||
action = req.get('action')
|
||||
parameters = req.get('parameters')
|
||||
# contexts = req.get('contexts')
|
||||
response = ApiaiResponse(parameters)
|
||||
apiai_response = ApiaiResponse(parameters)
|
||||
|
||||
# Default Welcome Intent
|
||||
# Maybe is better to handle this in api.ai directly?
|
||||
#
|
||||
# if intent == 'input.welcome':
|
||||
# response.add_speech(
|
||||
# "Hello, and welcome to the future. How may I help?")
|
||||
# return self.json(response)
|
||||
|
||||
if intent == "":
|
||||
if action == "":
|
||||
_LOGGER.warning("Received intent with empty action")
|
||||
response.add_speech(
|
||||
apiai_response.add_speech(
|
||||
"You have not defined an action in your api.ai intent.")
|
||||
return self.json(response)
|
||||
return self.json(apiai_response)
|
||||
|
||||
config = self.intents.get(intent)
|
||||
try:
|
||||
intent_response = yield from intent.async_handle(
|
||||
hass, DOMAIN, action,
|
||||
{key: {'value': value} for key, value
|
||||
in parameters.items()})
|
||||
|
||||
if config is None:
|
||||
_LOGGER.warning("Received unknown intent %s", intent)
|
||||
response.add_speech(
|
||||
"Intent '%s' is not yet configured within Home Assistant." %
|
||||
intent)
|
||||
return self.json(response)
|
||||
except intent.UnknownIntent as err:
|
||||
_LOGGER.warning('Received unknown intent %s', action)
|
||||
apiai_response.add_speech(
|
||||
"This intent is not yet configured within Home Assistant.")
|
||||
return self.json(apiai_response)
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
action = config.get(CONF_ACTION)
|
||||
async_action = config.get(CONF_ASYNC_ACTION)
|
||||
except intent.InvalidSlotInfo as err:
|
||||
_LOGGER.error('Received invalid slot data: %s', err)
|
||||
return self.json_message('Invalid slot data received',
|
||||
HTTP_BAD_REQUEST)
|
||||
except intent.IntentError:
|
||||
_LOGGER.exception('Error handling request for %s', action)
|
||||
return self.json_message('Error handling intent', HTTP_BAD_REQUEST)
|
||||
|
||||
if action is not None:
|
||||
# API.AI expects a response in less than 5s
|
||||
if async_action:
|
||||
# Do not wait for the action to be executed.
|
||||
# Needed if the action will take longer than 5s to execute
|
||||
self.hass.async_add_job(action.async_run(response.parameters))
|
||||
else:
|
||||
# Wait for the action to be executed so we can use results to
|
||||
# render the answer
|
||||
yield from action.async_run(response.parameters)
|
||||
if 'plain' in intent_response.speech:
|
||||
apiai_response.add_speech(
|
||||
intent_response.speech['plain']['speech'])
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(speech)
|
||||
|
||||
return self.json(response)
|
||||
return self.json(apiai_response)
|
||||
|
||||
|
||||
class ApiaiResponse(object):
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Support for Apple TV.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/apple_tv/
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.components.discovery import SERVICE_APPLE_TV
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'apple_tv'
|
||||
|
||||
SERVICE_SCAN = 'apple_tv_scan'
|
||||
SERVICE_AUTHENTICATE = 'apple_tv_authenticate'
|
||||
|
||||
ATTR_ATV = 'atv'
|
||||
ATTR_POWER = 'power'
|
||||
|
||||
CONF_LOGIN_ID = 'login_id'
|
||||
CONF_START_OFF = 'start_off'
|
||||
CONF_CREDENTIALS = 'credentials'
|
||||
|
||||
DEFAULT_NAME = 'Apple TV'
|
||||
|
||||
DATA_APPLE_TV = 'data_apple_tv'
|
||||
DATA_ENTITIES = 'data_apple_tv_entities'
|
||||
|
||||
KEY_CONFIG = 'apple_tv_configuring'
|
||||
|
||||
NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification'
|
||||
NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_CREDENTIALS, default=None): cv.string,
|
||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
# Currently no attributes but it might change later
|
||||
APPLE_TV_SCAN_SCHEMA = vol.Schema({})
|
||||
|
||||
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
def request_configuration(hass, config, atv, credentials):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
@asyncio.coroutine
|
||||
def configuration_callback(callback_data):
|
||||
"""Handle the submitted configuration."""
|
||||
from pyatv import exceptions
|
||||
pin = callback_data.get('pin')
|
||||
|
||||
try:
|
||||
yield from atv.airplay.finish_authentication(pin)
|
||||
hass.components.persistent_notification.async_create(
|
||||
'Authentication succeeded!<br /><br />Add the following '
|
||||
'to credentials: in your apple_tv configuration:<br /><br />'
|
||||
'{0}'.format(credentials),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
except exceptions.DeviceAuthenticationError as ex:
|
||||
hass.components.persistent_notification.async_create(
|
||||
'Authentication failed! Did you enter correct PIN?<br /><br />'
|
||||
'Details: {0}'.format(ex),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
|
||||
hass.async_add_job(configurator.request_done, instance)
|
||||
|
||||
instance = configurator.request_config(
|
||||
hass, 'Apple TV Authentication', configuration_callback,
|
||||
description='Please enter PIN code shown on screen.',
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def scan_for_apple_tvs(hass):
|
||||
"""Scan for devices and present a notification of the ones found."""
|
||||
import pyatv
|
||||
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
|
||||
|
||||
devices = []
|
||||
for atv in atvs:
|
||||
login_id = atv.login_id
|
||||
if login_id is None:
|
||||
login_id = 'Home Sharing disabled'
|
||||
devices.append('Name: {0}<br />Host: {1}<br />Login ID: {2}'.format(
|
||||
atv.name, atv.address, login_id))
|
||||
|
||||
if not devices:
|
||||
devices = ['No device(s) found']
|
||||
|
||||
hass.components.persistent_notification.async_create(
|
||||
'The following devices were found:<br /><br />' +
|
||||
'<br /><br />'.join(devices),
|
||||
title=NOTIFICATION_SCAN_TITLE,
|
||||
notification_id=NOTIFICATION_SCAN_ID)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the Apple TV component."""
|
||||
if DATA_APPLE_TV not in hass.data:
|
||||
hass.data[DATA_APPLE_TV] = {}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
"""Handler for service calls."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if entity_ids:
|
||||
devices = [device for device in hass.data[DATA_ENTITIES]
|
||||
if device.entity_id in entity_ids]
|
||||
else:
|
||||
devices = hass.data[DATA_ENTITIES]
|
||||
|
||||
for device in devices:
|
||||
atv = device.atv
|
||||
if service.service == SERVICE_AUTHENTICATE:
|
||||
credentials = yield from atv.airplay.generate_credentials()
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
_LOGGER.debug('Generated new credentials: %s', credentials)
|
||||
yield from atv.airplay.start_authentication()
|
||||
hass.async_add_job(request_configuration,
|
||||
hass, config, atv, credentials)
|
||||
elif service.service == SERVICE_SCAN:
|
||||
hass.async_add_job(scan_for_apple_tvs, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def atv_discovered(service, info):
|
||||
"""Setup an Apple TV that was auto discovered."""
|
||||
yield from _setup_atv(hass, {
|
||||
CONF_NAME: info['name'],
|
||||
CONF_HOST: info['host'],
|
||||
CONF_LOGIN_ID: info['properties']['hG'],
|
||||
CONF_START_OFF: False
|
||||
})
|
||||
|
||||
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
||||
|
||||
tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||
descriptions.get(SERVICE_SCAN),
|
||||
schema=APPLE_TV_SCAN_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
|
||||
descriptions.get(SERVICE_AUTHENTICATE),
|
||||
schema=APPLE_TV_AUTHENTICATE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _setup_atv(hass, atv_config):
|
||||
"""Setup an Apple TV."""
|
||||
import pyatv
|
||||
name = atv_config.get(CONF_NAME)
|
||||
host = atv_config.get(CONF_HOST)
|
||||
login_id = atv_config.get(CONF_LOGIN_ID)
|
||||
start_off = atv_config.get(CONF_START_OFF)
|
||||
credentials = atv_config.get(CONF_CREDENTIALS)
|
||||
|
||||
if host in hass.data[DATA_APPLE_TV]:
|
||||
return
|
||||
|
||||
details = pyatv.AppleTVDevice(name, host, login_id)
|
||||
session = async_get_clientsession(hass)
|
||||
atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
|
||||
if credentials:
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
|
||||
power = AppleTVPowerManager(hass, atv, start_off)
|
||||
hass.data[DATA_APPLE_TV][host] = {
|
||||
ATTR_ATV: atv,
|
||||
ATTR_POWER: power
|
||||
}
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'media_player', DOMAIN, atv_config))
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'remote', DOMAIN, atv_config))
|
||||
|
||||
|
||||
class AppleTVPowerManager:
|
||||
"""Manager for global power management of an Apple TV.
|
||||
|
||||
An instance is used per device to share the same power state between
|
||||
several platforms.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, atv, is_off):
|
||||
"""Initialize power manager."""
|
||||
self.hass = hass
|
||||
self.atv = atv
|
||||
self.listeners = []
|
||||
self._is_on = not is_off
|
||||
|
||||
def init(self):
|
||||
"""Initialize power management."""
|
||||
if self._is_on:
|
||||
self.atv.push_updater.start()
|
||||
|
||||
@property
|
||||
def turned_on(self):
|
||||
"""If device is on or off."""
|
||||
return self._is_on
|
||||
|
||||
def set_power_on(self, value):
|
||||
"""Change if a device is on or off."""
|
||||
if value != self._is_on:
|
||||
self._is_on = value
|
||||
if not self._is_on:
|
||||
self.atv.push_updater.stop()
|
||||
else:
|
||||
self.atv.push_updater.start()
|
||||
|
||||
for listener in self.listeners:
|
||||
self.hass.async_add_job(listener.async_update_ha_state())
|
||||
@@ -9,7 +9,6 @@ import logging
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
@@ -40,7 +39,6 @@ def setup(hass, config):
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
from pyarlo import PyArlo
|
||||
|
||||
@@ -50,8 +48,8 @@ def setup(hass, config):
|
||||
hass.data[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 />'
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
|
||||
@@ -13,6 +13,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import CoreState
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
@@ -105,6 +106,7 @@ TRIGGER_SERVICE_SCHEMA = vol.Schema({
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass, entity_id):
|
||||
"""
|
||||
Return true if specified automation entity_id is on.
|
||||
@@ -114,35 +116,41 @@ def is_on(hass, entity_id):
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_on(hass, entity_id=None):
|
||||
"""Turn on specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_off(hass, entity_id=None):
|
||||
"""Turn off specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def toggle(hass, entity_id=None):
|
||||
"""Toggle specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def trigger(hass, entity_id=None):
|
||||
"""Trigger specified automation or all."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_TRIGGER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def reload(hass):
|
||||
"""Reload the automation from config."""
|
||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def async_reload(hass):
|
||||
"""Reload the automation from config.
|
||||
|
||||
|
||||
@@ -12,13 +12,11 @@ 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'
|
||||
CONF_FROM = 'from'
|
||||
CONF_TO = 'to'
|
||||
CONF_STATE = 'state'
|
||||
CONF_FOR = 'for'
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(
|
||||
@@ -28,11 +26,9 @@ TRIGGER_SCHEMA = vol.All(
|
||||
# These are str on purpose. Want to catch YAML conversions
|
||||
CONF_FROM: str,
|
||||
CONF_TO: str,
|
||||
CONF_STATE: str,
|
||||
CONF_FOR: vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}),
|
||||
vol.Any(cv.key_dependency(CONF_FOR, CONF_TO),
|
||||
cv.key_dependency(CONF_FOR, CONF_STATE))
|
||||
cv.key_dependency(CONF_FOR, CONF_TO),
|
||||
)
|
||||
|
||||
|
||||
@@ -41,7 +37,7 @@ 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 = get_deprecated(config, CONF_TO, CONF_STATE, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_state_for_listener = None
|
||||
|
||||
@@ -42,8 +42,6 @@ def async_trigger(hass, config, action):
|
||||
},
|
||||
})
|
||||
|
||||
# Do something to call action
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
return async_track_sunrise(hass, call_action, offset)
|
||||
else:
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_AT, CONF_PLATFORM, CONF_AFTER
|
||||
from homeassistant.const import CONF_AT, CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
@@ -23,12 +23,10 @@ _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_AT, CONF_AFTER))
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -37,11 +35,6 @@ def async_trigger(hass, config, action):
|
||||
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)
|
||||
|
||||
@@ -21,7 +21,6 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
|
||||
REQUIREMENTS = ['axis==8']
|
||||
@@ -79,7 +78,7 @@ SERVICE_SCHEMA = vol.Schema({
|
||||
|
||||
def request_configuration(hass, name, host, serialnumber):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = get_component('configurator')
|
||||
configurator = hass.components.configurator
|
||||
|
||||
def configuration_callback(callback_data):
|
||||
"""Called when config is submitted."""
|
||||
@@ -242,12 +241,11 @@ def setup_device(hass, config):
|
||||
if enable_metadatastream:
|
||||
device.initialize_new_event = event_initialized
|
||||
if not device.initiate_metadatastream():
|
||||
notification = get_component('persistent_notification')
|
||||
notification.create(hass,
|
||||
'Dependency missing for sensors, '
|
||||
'please check documentation',
|
||||
title=DOMAIN,
|
||||
notification_id='axis_notification')
|
||||
hass.components.persistent_notification.create(
|
||||
'Dependency missing for sensors, '
|
||||
'please check documentation',
|
||||
title=DOMAIN,
|
||||
notification_id='axis_notification')
|
||||
|
||||
AXIS_DEVICES[device.serial_number] = device
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
if self._pin is not None:
|
||||
request = requests.get(
|
||||
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
|
||||
if request.status_code is not 200:
|
||||
if request.status_code != 200:
|
||||
_LOGGER.error("Can't set mode of %s", self._resource)
|
||||
|
||||
@property
|
||||
|
||||
@@ -199,11 +199,10 @@ class FlicButton(BinarySensorDevice):
|
||||
"Queued %s dropped for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return True
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
|
||||
def _on_up_down(self, channel, click_type, was_queued, time_diff):
|
||||
"""Update device state, if event was not queued."""
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.2']
|
||||
REQUIREMENTS = ['pyhik==0.1.3']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
|
||||
@@ -34,8 +34,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.modbus as modbus
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_NAME, CONF_SLAVE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
@@ -18,7 +18,6 @@ DEPENDENCIES = ['modbus']
|
||||
|
||||
CONF_COIL = 'coil'
|
||||
CONF_COILS = 'coils'
|
||||
CONF_SLAVE = 'slave'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COILS): [{
|
||||
@@ -50,6 +49,7 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
self._coil = int(coil)
|
||||
self._value = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
@@ -62,4 +62,10 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
def update(self):
|
||||
"""Update the state of the sensor."""
|
||||
result = modbus.HUB.read_coils(self._slave, self._coil, 1)
|
||||
self._value = result.bits[0]
|
||||
try:
|
||||
self._value = result.bits[0]
|
||||
except AttributeError:
|
||||
_LOGGER.error(
|
||||
'No response from modbus slave %s coil %s',
|
||||
self._slave,
|
||||
self._coil)
|
||||
|
||||
@@ -156,8 +156,7 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
|
||||
elif self._cameratype == 'NOC':
|
||||
return PRESENCE_SENSOR_TYPES.get(self._sensor_name)
|
||||
else:
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -12,13 +12,12 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['octoprint']
|
||||
|
||||
DOMAIN = "octoprint"
|
||||
DEFAULT_NAME = 'OctoPrint'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
@@ -37,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the available OctoPrint binary sensors."""
|
||||
octoprint = get_component('octoprint')
|
||||
octoprint_api = hass.data[DOMAIN]["api"]
|
||||
name = config.get(CONF_NAME)
|
||||
monitored_conditions = config.get(
|
||||
CONF_MONITORED_CONDITIONS, SENSOR_TYPES.keys())
|
||||
@@ -45,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices = []
|
||||
for octo_type in monitored_conditions:
|
||||
new_sensor = OctoPrintBinarySensor(
|
||||
octoprint.OCTOPRINT, octo_type, SENSOR_TYPES[octo_type][2],
|
||||
octoprint_api, octo_type, SENSOR_TYPES[octo_type][2],
|
||||
name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0],
|
||||
SENSOR_TYPES[octo_type][1], 'flags')
|
||||
devices.append(new_sensor)
|
||||
@@ -98,6 +97,3 @@ class OctoPrintBinarySensor(BinarySensorDevice):
|
||||
except requests.exceptions.ConnectionError:
|
||||
# Error calling the api, already logged in api.update()
|
||||
return
|
||||
|
||||
if self._state is None:
|
||||
_LOGGER.warning("Unable to locate value for %s", self.sensor_type)
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.util import dt as dt_util
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_VARIABLE = 'variable'
|
||||
CONF_RESET_DELAY_SEC = 'reset_delay_sec'
|
||||
|
||||
DEFAULT_NAME = 'Pilight Binary Sensor'
|
||||
DEPENDENCIES = ['pilight']
|
||||
@@ -38,7 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default='on'): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default='off'): cv.string,
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean,
|
||||
vol.Optional(CONF_RESET_DELAY_SEC, default=30): cv.positive_int
|
||||
})
|
||||
|
||||
|
||||
@@ -54,6 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
payload=config.get(CONF_PAYLOAD),
|
||||
on_value=config.get(CONF_PAYLOAD_ON),
|
||||
off_value=config.get(CONF_PAYLOAD_OFF),
|
||||
rst_dly_sec=config.get(CONF_RESET_DELAY_SEC),
|
||||
)])
|
||||
else:
|
||||
add_devices([PilightBinarySensor(
|
||||
|
||||
@@ -35,6 +35,9 @@ SCAN_INTERVAL = timedelta(minutes=5)
|
||||
PING_MATCHER = re.compile(
|
||||
r'(?P<min>\d+.\d+)\/(?P<avg>\d+.\d+)\/(?P<max>\d+.\d+)\/(?P<mdev>\d+.\d+)')
|
||||
|
||||
PING_MATCHER_BUSYBOX = re.compile(
|
||||
r'(?P<min>\d+.\d+)\/(?P<avg>\d+.\d+)\/(?P<max>\d+.\d+)')
|
||||
|
||||
WIN32_PING_MATCHER = re.compile(
|
||||
r'(?P<min>\d+)ms.+(?P<max>\d+)ms.+(?P<avg>\d+)ms')
|
||||
|
||||
@@ -126,14 +129,21 @@ class PingData(object):
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': ''}
|
||||
else:
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
if 'max/' not in str(out):
|
||||
match = PING_MATCHER_BUSYBOX.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
'mdev': ''}
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
except (subprocess.CalledProcessError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
if self.rest.data is None:
|
||||
return False
|
||||
|
||||
response = self.rest.data
|
||||
|
||||
if self._value_template is not None:
|
||||
response = self._value_template.\
|
||||
async_render_with_possible_json_value(self.rest.data, False)
|
||||
|
||||
@@ -62,6 +62,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
entity[CONF_COMMAND_ON],
|
||||
entity[CONF_COMMAND_OFF])
|
||||
device.hass = hass
|
||||
device.is_lighting4 = (packet_id[2:4] == '13')
|
||||
sensors.append(device)
|
||||
rfxtrx.RFX_DEVICES[device_id] = device
|
||||
|
||||
@@ -94,6 +95,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
sensor = RfxtrxBinarySensor(event, pkt_id)
|
||||
sensor.hass = hass
|
||||
sensor.is_lighting4 = (pkt_id[2:4] == '13')
|
||||
rfxtrx.RFX_DEVICES[device_id] = sensor
|
||||
add_devices_callback([sensor])
|
||||
_LOGGER.info("Added binary sensor %s "
|
||||
@@ -111,12 +114,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype)
|
||||
|
||||
if sensor.is_pt2262:
|
||||
cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits)
|
||||
_LOGGER.info("applying cmd %s to device_id: %s)",
|
||||
cmd, sensor.masked_id)
|
||||
sensor.apply_cmd(int(cmd, 16))
|
||||
if sensor.is_lighting4:
|
||||
if sensor.data_bits is not None:
|
||||
cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits)
|
||||
sensor.apply_cmd(int(cmd, 16))
|
||||
else:
|
||||
sensor.update_state(True)
|
||||
else:
|
||||
rfxtrx.apply_received_command(event)
|
||||
|
||||
@@ -151,6 +154,7 @@ class RfxtrxBinarySensor(BinarySensorDevice):
|
||||
self._device_class = device_class
|
||||
self._off_delay = off_delay
|
||||
self._state = False
|
||||
self.is_lighting4 = False
|
||||
self.delay_listener = None
|
||||
self._data_bits = data_bits
|
||||
self._cmd_on = cmd_on
|
||||
@@ -170,11 +174,6 @@ class RfxtrxBinarySensor(BinarySensorDevice):
|
||||
"""Return the device name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_pt2262(self):
|
||||
"""Return true if the device is PT2262-based."""
|
||||
return self._data_bits is not None
|
||||
|
||||
@property
|
||||
def masked_id(self):
|
||||
"""Return the masked device id (isolated address bits)."""
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Support for Velbus Binary Sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.velbus/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_DEVICES
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.components.velbus import DOMAIN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
DEPENDENCIES = ['velbus']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
|
||||
{
|
||||
vol.Required('module'): cv.positive_int,
|
||||
vol.Required('channel'): cv.positive_int,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional('is_pushbutton'): cv.boolean
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Velbus binary sensors."""
|
||||
velbus = hass.data[DOMAIN]
|
||||
|
||||
add_devices(VelbusBinarySensor(sensor, velbus)
|
||||
for sensor in config[CONF_DEVICES])
|
||||
|
||||
|
||||
class VelbusBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Velbus Binary Sensor."""
|
||||
|
||||
def __init__(self, binary_sensor, velbus):
|
||||
"""Initialize a Velbus light."""
|
||||
self._velbus = velbus
|
||||
self._name = binary_sensor[CONF_NAME]
|
||||
self._module = binary_sensor['module']
|
||||
self._channel = binary_sensor['channel']
|
||||
self._is_pushbutton = 'is_pushbutton' in binary_sensor \
|
||||
and binary_sensor['is_pushbutton']
|
||||
self._state = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add listener for Velbus messages on bus."""
|
||||
yield from self.hass.async_add_job(
|
||||
self._velbus.subscribe, self._on_message)
|
||||
|
||||
def _on_message(self, message):
|
||||
import velbus
|
||||
if isinstance(message, velbus.PushButtonStatusMessage):
|
||||
if message.address == self._module and \
|
||||
self._channel in message.get_channels():
|
||||
if self._is_pushbutton:
|
||||
if self._channel in message.closed:
|
||||
self._toggle()
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
self._toggle()
|
||||
|
||||
def _toggle(self):
|
||||
if self._state is True:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
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 sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the sensor is on."""
|
||||
return self._state
|
||||
@@ -30,8 +30,7 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
|
||||
return bool(val)
|
||||
elif self._attribute in ['doors', 'windows']:
|
||||
return any([val[key] for key in val if 'Open' in key])
|
||||
else:
|
||||
return val != 'Normal'
|
||||
return val != 'Normal'
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -121,10 +121,6 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Smoke detector."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -136,10 +132,6 @@ class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
class WinkHub(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Hub."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -152,10 +144,6 @@ class WinkHub(WinkBinarySensorDevice):
|
||||
class WinkRemote(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Lutron Connected bulb remote."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -175,10 +163,6 @@ class WinkRemote(WinkBinarySensorDevice):
|
||||
class WinkButton(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay button."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@@ -191,10 +175,6 @@ class WinkButton(WinkBinarySensorDevice):
|
||||
class WinkGang(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay gang."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the gang is connected."""
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
"""Support for Xiaomi binary sensors."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.xiaomi import (PY_XIAOMI_GATEWAY, XiaomiDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NO_CLOSE = 'no_close'
|
||||
ATTR_OPEN_SINCE = 'Open since'
|
||||
|
||||
MOTION = 'motion'
|
||||
NO_MOTION = 'no_motion'
|
||||
ATTR_NO_MOTION_SINCE = 'No motion since'
|
||||
|
||||
DENSITY = 'density'
|
||||
ATTR_DENSITY = 'Density'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Xiaomi devices."""
|
||||
devices = []
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices['binary_sensor']:
|
||||
model = device['model']
|
||||
if model == 'motion':
|
||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||
elif model == 'sensor_motion.aq2':
|
||||
devices.append(XiaomiMotionSensor(device, hass, gateway))
|
||||
elif model == 'magnet':
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'sensor_magnet.aq2':
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'smoke':
|
||||
devices.append(XiaomiSmokeSensor(device, gateway))
|
||||
elif model == 'natgas':
|
||||
devices.append(XiaomiNatgasSensor(device, gateway))
|
||||
elif model == 'switch':
|
||||
devices.append(XiaomiButton(device, 'Switch', 'status',
|
||||
hass, gateway))
|
||||
elif model == 'sensor_switch.aq2':
|
||||
devices.append(XiaomiButton(device, 'Switch', 'status',
|
||||
hass, gateway))
|
||||
elif model == '86sw1':
|
||||
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
|
||||
hass, gateway))
|
||||
elif model == '86sw2':
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
|
||||
'channel_0', hass, gateway))
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
|
||||
'channel_1', hass, gateway))
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Both)',
|
||||
'dual_channel', hass, gateway))
|
||||
elif model == 'cube':
|
||||
devices.append(XiaomiCube(device, hass, gateway))
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class XiaomiBinarySensor(XiaomiDevice, BinarySensorDevice):
|
||||
"""Representation of a base XiaomiBinarySensor."""
|
||||
|
||||
def __init__(self, device, name, xiaomi_hub, data_key, device_class):
|
||||
"""Initialize the XiaomiSmokeSensor."""
|
||||
self._data_key = data_key
|
||||
self._device_class = device_class
|
||||
self._should_poll = False
|
||||
self._density = 0
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return self._should_poll
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of binary sensor."""
|
||||
return self._device_class
|
||||
|
||||
def update(self):
|
||||
"""Update the sensor state."""
|
||||
_LOGGER.debug('Updating xiaomi sensor by polling')
|
||||
self._get_from_hub(self._sid)
|
||||
|
||||
|
||||
class XiaomiNatgasSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiNatgasSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
"""Initialize the XiaomiSmokeSensor."""
|
||||
self._density = None
|
||||
XiaomiBinarySensor.__init__(self, device, 'Natgas Sensor', xiaomi_hub,
|
||||
'alarm', 'gas')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_DENSITY: self._density}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
if DENSITY in data:
|
||||
self._density = int(data.get(DENSITY))
|
||||
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == '1':
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == '0':
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiMotionSensor."""
|
||||
|
||||
def __init__(self, device, hass, xiaomi_hub):
|
||||
"""Initialize the XiaomiMotionSensor."""
|
||||
self._hass = hass
|
||||
self._no_motion_since = 0
|
||||
XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub,
|
||||
'status', 'motion')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_NO_MOTION_SINCE: self._no_motion_since}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
self._should_poll = False
|
||||
if NO_MOTION in data: # handle push from the hub
|
||||
self._no_motion_since = data[NO_MOTION]
|
||||
self._state = False
|
||||
return True
|
||||
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == MOTION:
|
||||
self._should_poll = True
|
||||
if self.entity_id is not None:
|
||||
self._hass.bus.fire('motion', {
|
||||
'entity_id': self.entity_id
|
||||
})
|
||||
|
||||
self._no_motion_since = 0
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == NO_MOTION:
|
||||
if not self._state:
|
||||
return False
|
||||
self._state = False
|
||||
return True
|
||||
|
||||
|
||||
class XiaomiDoorSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiDoorSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
"""Initialize the XiaomiDoorSensor."""
|
||||
self._open_since = 0
|
||||
XiaomiBinarySensor.__init__(self, device, 'Door Window Sensor',
|
||||
xiaomi_hub, 'status', 'opening')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_OPEN_SINCE: self._open_since}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
self._should_poll = False
|
||||
if NO_CLOSE in data: # handle push from the hub
|
||||
self._open_since = data[NO_CLOSE]
|
||||
return True
|
||||
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == 'open':
|
||||
self._should_poll = True
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == 'close':
|
||||
self._open_since = 0
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiSmokeSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiSmokeSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
"""Initialize the XiaomiSmokeSensor."""
|
||||
self._density = 0
|
||||
XiaomiBinarySensor.__init__(self, device, 'Smoke Sensor', xiaomi_hub,
|
||||
'alarm', 'smoke')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {ATTR_DENSITY: self._density}
|
||||
attrs.update(super().device_state_attributes)
|
||||
return attrs
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
if DENSITY in data:
|
||||
self._density = int(data.get(DENSITY))
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == '1':
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == '0':
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiButton(XiaomiBinarySensor):
|
||||
"""Representation of a Xiaomi Button."""
|
||||
|
||||
def __init__(self, device, name, data_key, hass, xiaomi_hub):
|
||||
"""Initialize the XiaomiButton."""
|
||||
self._hass = hass
|
||||
XiaomiBinarySensor.__init__(self, device, name, xiaomi_hub,
|
||||
data_key, None)
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == 'long_click_press':
|
||||
self._state = True
|
||||
click_type = 'long_click_press'
|
||||
elif value == 'long_click_release':
|
||||
self._state = False
|
||||
click_type = 'hold'
|
||||
elif value == 'click':
|
||||
click_type = 'single'
|
||||
elif value == 'double_click':
|
||||
click_type = 'double'
|
||||
elif value == 'both_click':
|
||||
click_type = 'both'
|
||||
else:
|
||||
return False
|
||||
|
||||
self._hass.bus.fire('click', {
|
||||
'entity_id': self.entity_id,
|
||||
'click_type': click_type
|
||||
})
|
||||
if value in ['long_click_press', 'long_click_release']:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiCube(XiaomiBinarySensor):
|
||||
"""Representation of a Xiaomi Cube."""
|
||||
|
||||
def __init__(self, device, hass, xiaomi_hub):
|
||||
"""Initialize the Xiaomi Cube."""
|
||||
self._hass = hass
|
||||
self._state = False
|
||||
XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub,
|
||||
None, None)
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
if 'status' in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': data['status']
|
||||
})
|
||||
|
||||
if 'rotate' in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': 'rotate',
|
||||
'action_value': float(data['rotate'].replace(",", "."))
|
||||
})
|
||||
return False
|
||||
@@ -34,10 +34,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||
|
||||
clusters = discovery_info['clusters']
|
||||
in_clusters = discovery_info['in_clusters']
|
||||
|
||||
device_class = None
|
||||
cluster = [c for c in clusters if isinstance(c, IasZone)][0]
|
||||
cluster = in_clusters[IasZone.cluster_id]
|
||||
if discovery_info['new_join']:
|
||||
yield from cluster.bind()
|
||||
ieee = cluster.endpoint.device.application.ieee
|
||||
@@ -64,7 +64,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
super().__init__(**kwargs)
|
||||
self._device_class = device_class
|
||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||
self._ias_zone_cluster = self._clusters[IasZone.cluster_id]
|
||||
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -148,8 +148,7 @@ class CalendarEventDevice(Entity):
|
||||
if 'date' in date:
|
||||
return dt.start_of_local_day(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time.min))
|
||||
else:
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -55,6 +56,7 @@ CAMERA_SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def enable_motion_detection(hass, entity_id=None):
|
||||
"""Enable Motion Detection."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
@@ -62,6 +64,7 @@ def enable_motion_detection(hass, entity_id=None):
|
||||
DOMAIN, SERVICE_EN_MOTION, data))
|
||||
|
||||
|
||||
@bind_hass
|
||||
def disable_motion_detection(hass, entity_id=None):
|
||||
"""Disable Motion Detection."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
@@ -266,8 +269,7 @@ class Camera(Entity):
|
||||
return STATE_RECORDING
|
||||
elif self.is_streaming:
|
||||
return STATE_STREAMING
|
||||
else:
|
||||
return STATE_IDLE
|
||||
return STATE_IDLE
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in the camera."""
|
||||
|
||||
@@ -7,108 +7,59 @@ https://home-assistant.io/components/camera.amcrest/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.amcrest import (
|
||||
STREAM_SOURCE_LIST, TIMEOUT)
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web,
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.0']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
DEPENDENCIES = ['amcrest', 'ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'mjpeg'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
TIMEOUT = 5
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up an Amcrest IP Camera."""
|
||||
from amcrest import AmcrestCamera
|
||||
camera = AmcrestCamera(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
camera.current_time
|
||||
# pylint: disable=broad-except
|
||||
except Exception as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %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
|
||||
device = discovery_info['device']
|
||||
authentication = discovery_info['authentication']
|
||||
ffmpeg_arguments = discovery_info['ffmpeg_arguments']
|
||||
name = discovery_info['name']
|
||||
resolution = discovery_info['resolution']
|
||||
stream_source = discovery_info['stream_source']
|
||||
|
||||
async_add_devices([
|
||||
AmcrestCam(hass,
|
||||
name,
|
||||
device,
|
||||
authentication,
|
||||
ffmpeg_arguments,
|
||||
stream_source,
|
||||
resolution)], True)
|
||||
|
||||
add_devices([AmcrestCam(hass, config, camera)])
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, device_info, camera):
|
||||
def __init__(self, hass, name, camera, authentication,
|
||||
ffmpeg_arguments, stream_source, resolution):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = name
|
||||
self._camera = camera
|
||||
self._base_url = self._camera.get_base_url()
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
|
||||
self._stream_source = STREAM_SOURCE_LIST[
|
||||
device_info.get(CONF_STREAM_SOURCE)
|
||||
]
|
||||
self._token = self._auth = aiohttp.BasicAuth(
|
||||
device_info.get(CONF_USERNAME),
|
||||
password=device_info.get(CONF_PASSWORD)
|
||||
)
|
||||
self._ffmpeg_arguments = ffmpeg_arguments
|
||||
self._stream_source = stream_source
|
||||
self._resolution = resolution
|
||||
self._token = self._auth = authentication
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
|
||||
@@ -49,7 +49,6 @@ class ArloCam(Camera):
|
||||
"""Initialize an Arlo camera."""
|
||||
super().__init__()
|
||||
self._camera = camera
|
||||
self._base_stn = hass.data['arlo'].base_stations[0]
|
||||
self._name = self._camera.name
|
||||
self._motion_status = False
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
@@ -103,7 +102,16 @@ class ArloCam(Camera):
|
||||
|
||||
def set_base_station_mode(self, mode):
|
||||
"""Set the mode in the base station."""
|
||||
self._base_stn.mode = mode
|
||||
# Get the list of base stations identified by library
|
||||
base_stations = self.hass.data[DATA_ARLO].base_stations
|
||||
|
||||
# Some Arlo cameras does not have basestation
|
||||
# So check if there is base station detected first
|
||||
# if yes, then choose the primary base station
|
||||
# Set the mode on the chosen base station
|
||||
if base_stations:
|
||||
primary_base_station = base_stations[0]
|
||||
primary_base_station.mode = mode
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable the Motion detection in base station (Arm)."""
|
||||
|
||||
@@ -113,8 +113,7 @@ class NetatmoCamera(Camera):
|
||||
return "Presence"
|
||||
elif self._cameratype == "NACamera":
|
||||
return "Welcome"
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -54,6 +55,7 @@ class ONVIFCamera(Camera):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a ONVIF camera."""
|
||||
from onvif import ONVIFService
|
||||
import onvif
|
||||
super().__init__()
|
||||
|
||||
self._name = config.get(CONF_NAME)
|
||||
@@ -63,7 +65,7 @@ class ONVIFCamera(Camera):
|
||||
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)
|
||||
'{}/wsdl/media.wsdl'.format(os.path.dirname(onvif.__file__))
|
||||
)
|
||||
self._input = media.GetStreamUri().Uri
|
||||
_LOGGER.debug("ONVIF Camera Using the following URL for %s: %s",
|
||||
|
||||
@@ -54,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error("Unable to connect to NVR: %s", str(ex))
|
||||
return False
|
||||
|
||||
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid'
|
||||
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
|
||||
# Filter out airCam models, which are not supported in the latest
|
||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||
cameras = [
|
||||
|
||||
@@ -14,6 +14,7 @@ from numbers import Number
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -114,6 +115,7 @@ SET_SWING_MODE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_away_mode(hass, away_mode, entity_id=None):
|
||||
"""Turn all or specified climate devices away mode on."""
|
||||
data = {
|
||||
@@ -126,6 +128,7 @@ def set_away_mode(hass, away_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_hold_mode(hass, hold_mode, entity_id=None):
|
||||
"""Set new hold mode."""
|
||||
data = {
|
||||
@@ -138,6 +141,7 @@ def set_hold_mode(hass, hold_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_aux_heat(hass, aux_heat, entity_id=None):
|
||||
"""Turn all or specified climate devices auxillary heater on."""
|
||||
data = {
|
||||
@@ -150,6 +154,7 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_temperature(hass, temperature=None, entity_id=None,
|
||||
target_temp_high=None, target_temp_low=None,
|
||||
operation_mode=None):
|
||||
@@ -167,6 +172,7 @@ def set_temperature(hass, temperature=None, entity_id=None,
|
||||
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_humidity(hass, humidity, entity_id=None):
|
||||
"""Set new target humidity."""
|
||||
data = {ATTR_HUMIDITY: humidity}
|
||||
@@ -177,6 +183,7 @@ def set_humidity(hass, humidity, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_fan_mode(hass, fan, entity_id=None):
|
||||
"""Set all or specified climate devices fan mode on."""
|
||||
data = {ATTR_FAN_MODE: fan}
|
||||
@@ -187,6 +194,7 @@ def set_fan_mode(hass, fan, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_operation_mode(hass, operation_mode, entity_id=None):
|
||||
"""Set new target operation mode."""
|
||||
data = {ATTR_OPERATION_MODE: operation_mode}
|
||||
@@ -197,6 +205,7 @@ def set_operation_mode(hass, operation_mode, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
"""Set new target swing mode."""
|
||||
data = {ATTR_SWING_MODE: swing_mode}
|
||||
@@ -398,16 +407,14 @@ class ClimateDevice(Entity):
|
||||
"""Return the current state."""
|
||||
if self.current_operation:
|
||||
return self.current_operation
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
if self.unit_of_measurement == TEMP_CELSIUS:
|
||||
return PRECISION_TENTHS
|
||||
else:
|
||||
return PRECISION_WHOLE
|
||||
return PRECISION_WHOLE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@@ -709,6 +716,5 @@ class ClimateDevice(Entity):
|
||||
return round(temp * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(temp, 1)
|
||||
else:
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
|
||||
@@ -151,16 +151,14 @@ class Thermostat(ClimateDevice):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
@@ -171,8 +169,7 @@ class Thermostat(ClimateDevice):
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
elif self.current_operation == STATE_COOL:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def desired_fan_mode(self):
|
||||
@@ -184,8 +181,7 @@ class Thermostat(ClimateDevice):
|
||||
"""Return the current fan state."""
|
||||
if 'fan' in self.thermostat['equipmentStatus']:
|
||||
return STATE_ON
|
||||
else:
|
||||
return STATE_OFF
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
@@ -199,15 +195,13 @@ class Thermostat(ClimateDevice):
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# A temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
else:
|
||||
# A permanent hold from away climate is away_mode
|
||||
return None
|
||||
# A permanent hold from away climate is away_mode
|
||||
return None
|
||||
elif event['holdClimateRef'] != "":
|
||||
# Any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
else:
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
elif event['type'].startswith('auto'):
|
||||
# All auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
@@ -222,8 +216,7 @@ class Thermostat(ClimateDevice):
|
||||
if self.operation_mode == 'auxHeatOnly' or \
|
||||
self.operation_mode == 'heatPump':
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return self.operation_mode
|
||||
return self.operation_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@@ -384,8 +377,7 @@ class Thermostat(ClimateDevice):
|
||||
# add further conditions if other hold durations should be
|
||||
# supported; note that this should not include 'indefinite'
|
||||
# as an indefinite away hold is interpreted as away_mode
|
||||
else:
|
||||
return 'nextTransition'
|
||||
return 'nextTransition'
|
||||
|
||||
@property
|
||||
def climate_list(self):
|
||||
|
||||
@@ -145,4 +145,4 @@ class Flexit(ClimateDevice):
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new fan mode."""
|
||||
self.unit.set_fan_speed(fan)
|
||||
self.unit.set_fan_speed(self._fan_list.index(fan))
|
||||
|
||||
@@ -12,7 +12,8 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import switch
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
STATE_AUTO)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
|
||||
CONF_NAME)
|
||||
@@ -87,6 +88,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
self._keep_alive = keep_alive
|
||||
self._enabled = True
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@@ -131,18 +133,39 @@ class GenericThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self._enabled:
|
||||
return STATE_OFF
|
||||
if self.ac_mode:
|
||||
cooling = self._active and self._is_device_active
|
||||
return STATE_COOL if cooling else STATE_IDLE
|
||||
else:
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [STATE_AUTO, STATE_OFF]
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_AUTO:
|
||||
self._enabled = True
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
_LOGGER.error('Unrecognized operation mode: %s', operation_mode)
|
||||
return
|
||||
# Ensure we updae the current operation after changing the mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -159,9 +182,9 @@ class GenericThermostat(ClimateDevice):
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return ClimateDevice.min_temp.fget(self)
|
||||
|
||||
# get default temp from super class
|
||||
return ClimateDevice.min_temp.fget(self)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
@@ -169,9 +192,9 @@ class GenericThermostat(ClimateDevice):
|
||||
# pylint: disable=no-member
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
|
||||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_sensor_changed(self, entity_id, old_state, new_state):
|
||||
@@ -221,6 +244,9 @@ class GenericThermostat(ClimateDevice):
|
||||
if not self._active:
|
||||
return
|
||||
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if self.min_cycle_duration:
|
||||
if self._is_device_active:
|
||||
current_state = STATE_ON
|
||||
|
||||
@@ -46,8 +46,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if region == 'us':
|
||||
return _setup_us(username, password, config, add_devices)
|
||||
else:
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
|
||||
def _setup_round(username, password, config, add_devices):
|
||||
@@ -251,8 +251,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._device.system_mode == 'cool':
|
||||
return self._device.setpoint_cool
|
||||
else:
|
||||
return self._device.setpoint_heat
|
||||
return self._device.setpoint_heat
|
||||
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
|
||||
@@ -10,7 +10,6 @@ import logging
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.maxcube import MAXCUBE_HANDLE
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -140,7 +139,7 @@ class MaxCubeClimate(ClimateDevice):
|
||||
def map_temperature_max_hass(temperature):
|
||||
"""Map Temperature from MAX! to HASS."""
|
||||
if temperature is None:
|
||||
return STATE_UNKNOWN
|
||||
return 0.0
|
||||
|
||||
return temperature
|
||||
|
||||
|
||||
@@ -109,16 +109,14 @@ class NestThermostat(ClimateDevice):
|
||||
return self._mode
|
||||
elif self._mode == STATE_HEAT_COOL:
|
||||
return STATE_AUTO
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on:
|
||||
return self._target_temperature
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@@ -129,8 +127,7 @@ class NestThermostat(ClimateDevice):
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
@@ -141,8 +138,7 @@ class NestThermostat(ClimateDevice):
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
@@ -188,9 +184,8 @@ class NestThermostat(ClimateDevice):
|
||||
if self._has_fan:
|
||||
# Return whether the fan is on
|
||||
return STATE_ON if self._fan else STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
||||
@@ -119,14 +119,14 @@ class NetatmoThermostat(ClimateDevice):
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = False
|
||||
|
||||
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature for 2 hours."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
mode = "manual"
|
||||
self._data.thermostatdata.setthermpoint(
|
||||
mode, temperature, endTimeOffset)
|
||||
mode, temperature, DEFAULT_TIME_OFFSET)
|
||||
self._target_temperature = temperature
|
||||
self._away = False
|
||||
|
||||
|
||||
@@ -92,8 +92,7 @@ class ThermostatDevice(ClimateDevice):
|
||||
"""Return current operation i.e. heat, cool, idle."""
|
||||
if self._state:
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return STATE_IDLE
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
@@ -136,24 +136,53 @@ class RadioThermostat(ClimateDevice):
|
||||
return self._away
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
self._current_temperature = self.device.temp['raw']
|
||||
"""Update and validate the data from the thermostat."""
|
||||
current_temp = self.device.temp['raw']
|
||||
if current_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid temperature reading")
|
||||
return
|
||||
self._current_temperature = current_temp
|
||||
self._name = self.device.name['raw']
|
||||
self._fmode = self.device.fmode['human']
|
||||
self._tmode = self.device.tmode['human']
|
||||
self._tstate = self.device.tstate['human']
|
||||
try:
|
||||
self._fmode = self.device.fmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid fan mode reading")
|
||||
try:
|
||||
self._tmode = self.device.tmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat mode reading")
|
||||
try:
|
||||
self._tstate = self.device.tstate['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat state reading")
|
||||
|
||||
if self._tmode == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_COOL
|
||||
elif self._tmode == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_HEAT
|
||||
elif self._tmode == 'Auto':
|
||||
if self._tstate == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
elif self._tstate == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_AUTO
|
||||
else:
|
||||
self._current_operation = STATE_IDLE
|
||||
|
||||
@@ -117,9 +117,8 @@ class SensiboClimate(ClimateDevice):
|
||||
# We are working in same units as the a/c unit. Use whole degrees
|
||||
# like the API supports.
|
||||
return 1
|
||||
else:
|
||||
# Unit conversion is going on. No point to stick to specific steps.
|
||||
return None
|
||||
# Unit conversion is going on. No point to stick to specific steps.
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
@@ -52,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
zones = tado.get_zones()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to get zone info from mytado")
|
||||
return False
|
||||
return
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
@@ -61,9 +61,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if climate_devices:
|
||||
add_devices(climate_devices, True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
@@ -150,8 +147,7 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return current readable operation mode."""
|
||||
if self._cooling:
|
||||
return "Cooling"
|
||||
else:
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@@ -163,16 +159,14 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return the fan setting."""
|
||||
if self.ac_mode:
|
||||
return FAN_MODES_LIST.get(self._current_fan)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
if self.ac_mode:
|
||||
return list(FAN_MODES_LIST.values())
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
@@ -218,18 +212,16 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
@@ -281,31 +273,38 @@ class TadoClimate(ClimateDevice):
|
||||
else:
|
||||
self._device_is_active = True
|
||||
|
||||
overlay = False
|
||||
overlay_data = None
|
||||
termination = self._current_operation
|
||||
cooling = False
|
||||
fan_speed = CONST_MODE_OFF
|
||||
|
||||
if 'overlay' in data:
|
||||
overlay_data = data['overlay']
|
||||
overlay = overlay_data is not None
|
||||
|
||||
if overlay:
|
||||
termination = overlay_data['termination']['type']
|
||||
|
||||
if 'setting' in overlay_data:
|
||||
setting_data = overlay_data['setting']
|
||||
setting = setting is not None
|
||||
|
||||
if setting:
|
||||
if 'mode' in setting_data:
|
||||
cooling = setting_data['mode'] == 'COOL'
|
||||
|
||||
if 'fanSpeed' in setting_data:
|
||||
fan_speed = setting_data['fanSpeed']
|
||||
|
||||
if self._device_is_active:
|
||||
overlay = False
|
||||
overlay_data = None
|
||||
termination = self._current_operation
|
||||
cooling = False
|
||||
fan_speed = CONST_MODE_OFF
|
||||
|
||||
if 'overlay' in data:
|
||||
overlay_data = data['overlay']
|
||||
overlay = overlay_data is not None
|
||||
|
||||
if overlay:
|
||||
termination = overlay_data['termination']['type']
|
||||
|
||||
if 'setting' in overlay_data:
|
||||
cooling = overlay_data['setting']['mode'] == 'COOL'
|
||||
fan_speed = overlay_data['setting']['fanSpeed']
|
||||
|
||||
# If you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
self._cooling = cooling
|
||||
self._current_fan = fan_speed
|
||||
|
||||
self._cooling = cooling
|
||||
self._current_fan = fan_speed
|
||||
|
||||
def _control_heating(self):
|
||||
"""Send new target temperature to mytado."""
|
||||
|
||||
@@ -111,8 +111,8 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
# This will address both possibilities
|
||||
if self.wink.current_humidity() < 1:
|
||||
return self.wink.current_humidity() * 100
|
||||
else:
|
||||
return self.wink.current_humidity()
|
||||
return self.wink.current_humidity()
|
||||
return None
|
||||
|
||||
@property
|
||||
def external_temperature(self):
|
||||
@@ -175,10 +175,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return self.wink.current_max_set_point()
|
||||
elif self.current_operation == STATE_HEAT:
|
||||
return self.wink.current_min_set_point()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@@ -206,8 +203,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return True
|
||||
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -270,9 +266,8 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return STATE_ON
|
||||
elif self.wink.current_fan_mode() == 'auto':
|
||||
return STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
@@ -451,8 +446,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
return SPEED_MEDIUM
|
||||
elif speed <= 1.0 and speed > 0.8:
|
||||
return SPEED_HIGH
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
||||
@@ -154,8 +154,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
return TEMP_CELSIUS
|
||||
elif self._unit == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
return self._unit
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
||||
@@ -12,6 +12,7 @@ import logging
|
||||
from homeassistant.core import callback as async_callback
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
@@ -37,6 +38,7 @@ STATE_CONFIGURE = 'configure'
|
||||
STATE_CONFIGURED = 'configured'
|
||||
|
||||
|
||||
@bind_hass
|
||||
def request_config(
|
||||
hass, name, callback, description=None, description_image=None,
|
||||
submit_caption=None, fields=None, link_name=None, link_url=None,
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for functionality to have conversations with Home Assistant.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/conversation/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import warnings
|
||||
@@ -11,16 +12,17 @@ import warnings
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import script
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, HTTP_BAD_REQUEST)
|
||||
from homeassistant.helpers import intent, config_validation as cv
|
||||
from homeassistant.components import http
|
||||
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.15.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.15.1']
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
ATTR_SENTENCE = 'sentence'
|
||||
DOMAIN = 'conversation'
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
@@ -28,79 +30,174 @@ REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
SERVICE_PROCESS = 'process'
|
||||
|
||||
SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
|
||||
vol.Required(ATTR_TEXT): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
|
||||
cv.string: vol.Schema({
|
||||
vol.Required(ATTR_SENTENCE): cv.string,
|
||||
vol.Required('action'): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional('intents'): vol.Schema({
|
||||
cv.string: vol.All(cv.ensure_list, [cv.string])
|
||||
})
|
||||
})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup(hass, config):
|
||||
|
||||
@core.callback
|
||||
@bind_hass
|
||||
def async_register(hass, intent_type, utterances):
|
||||
"""Register an intent.
|
||||
|
||||
Registrations don't require conversations to be loaded. They will become
|
||||
active once the conversation component is loaded.
|
||||
"""
|
||||
intents = hass.data.get(DOMAIN)
|
||||
|
||||
if intents is None:
|
||||
intents = hass.data[DOMAIN] = {}
|
||||
|
||||
conf = intents.get(intent_type)
|
||||
|
||||
if conf is None:
|
||||
conf = intents[intent_type] = []
|
||||
|
||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Register the process service."""
|
||||
warnings.filterwarnings('ignore', module='fuzzywuzzy')
|
||||
from fuzzywuzzy import process as fuzzyExtract
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
config = config.get(DOMAIN, {})
|
||||
intents = hass.data.get(DOMAIN)
|
||||
|
||||
choices = {attrs[ATTR_SENTENCE]: script.Script(
|
||||
hass,
|
||||
attrs['action'],
|
||||
name)
|
||||
for name, attrs in config.items()}
|
||||
if intents is None:
|
||||
intents = hass.data[DOMAIN] = {}
|
||||
|
||||
for intent_type, utterances in config.get('intents', {}).items():
|
||||
conf = intents.get(intent_type)
|
||||
|
||||
if conf is None:
|
||||
conf = intents[intent_type] = []
|
||||
|
||||
conf.extend(_create_matcher(utterance) for utterance in utterances)
|
||||
|
||||
@asyncio.coroutine
|
||||
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)
|
||||
yield from _process(hass, text)
|
||||
|
||||
if not match:
|
||||
logger.error("Unable to process: %s", text)
|
||||
return
|
||||
|
||||
name, command = match.groups()
|
||||
entities = {state.entity_id: state.name for state in hass.states.all()}
|
||||
entity_ids = fuzzyExtract.extractOne(
|
||||
name, entities, score_cutoff=65)[2]
|
||||
|
||||
if not entity_ids:
|
||||
logger.error(
|
||||
"Could not find entity id %s from text %s", name, text)
|
||||
return
|
||||
|
||||
if command == 'on':
|
||||
hass.services.call(core.DOMAIN, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
}, blocking=True)
|
||||
|
||||
elif command == 'off':
|
||||
hass.services.call(core.DOMAIN, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
}, blocking=True)
|
||||
|
||||
else:
|
||||
logger.error('Got unsupported command %s from text %s',
|
||||
command, text)
|
||||
|
||||
hass.services.register(
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
|
||||
|
||||
hass.http.register_view(ConversationProcessView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _create_matcher(utterance):
|
||||
"""Create a regex that matches the utterance."""
|
||||
parts = re.split(r'({\w+})', utterance)
|
||||
group_matcher = re.compile(r'{(\w+)}')
|
||||
|
||||
pattern = ['^']
|
||||
|
||||
for part in parts:
|
||||
match = group_matcher.match(part)
|
||||
|
||||
if match is None:
|
||||
pattern.append(part)
|
||||
continue
|
||||
|
||||
pattern.append('(?P<{}>{})'.format(match.groups()[0], r'[\w ]+'))
|
||||
|
||||
pattern.append('$')
|
||||
return re.compile(''.join(pattern), re.I)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _process(hass, text):
|
||||
"""Process a line of text."""
|
||||
intents = hass.data.get(DOMAIN, {})
|
||||
|
||||
for intent_type, matchers in intents.items():
|
||||
for matcher in matchers:
|
||||
match = matcher.match(text)
|
||||
|
||||
if not match:
|
||||
continue
|
||||
|
||||
response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_type,
|
||||
{key: {'value': value} for key, value
|
||||
in match.groupdict().items()}, text)
|
||||
return response
|
||||
|
||||
from fuzzywuzzy import process as fuzzyExtract
|
||||
text = text.lower()
|
||||
match = REGEX_TURN_COMMAND.match(text)
|
||||
|
||||
if not match:
|
||||
_LOGGER.error("Unable to process: %s", text)
|
||||
return None
|
||||
|
||||
name, command = match.groups()
|
||||
entities = {state.entity_id: state.name for state
|
||||
in hass.states.async_all()}
|
||||
entity_ids = fuzzyExtract.extractOne(
|
||||
name, entities, score_cutoff=65)[2]
|
||||
|
||||
if not entity_ids:
|
||||
_LOGGER.error(
|
||||
"Could not find entity id %s from text %s", name, text)
|
||||
return None
|
||||
|
||||
if command == 'on':
|
||||
yield from hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
}, blocking=True)
|
||||
|
||||
elif command == 'off':
|
||||
yield from hass.services.async_call(
|
||||
core.DOMAIN, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity_ids,
|
||||
}, blocking=True)
|
||||
|
||||
else:
|
||||
_LOGGER.error('Got unsupported command %s from text %s',
|
||||
command, text)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ConversationProcessView(http.HomeAssistantView):
|
||||
"""View to retrieve shopping list content."""
|
||||
|
||||
url = '/api/conversation/process'
|
||||
name = "api:conversation:process"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Send a request for processing."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
text = data.get('text')
|
||||
|
||||
if text is None:
|
||||
return self.json_message('Missing "text" key in JSON.',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
intent_result = yield from _process(hass, text)
|
||||
|
||||
if intent_result is None:
|
||||
intent_result = intent.IntentResponse()
|
||||
intent_result.async_set_speech("Sorry, I didn't understand that")
|
||||
|
||||
return self.json(intent_result)
|
||||
|
||||
@@ -13,6 +13,7 @@ import os
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
@@ -22,7 +23,7 @@ from homeassistant.const import (
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
|
||||
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION, STATE_OPEN,
|
||||
STATE_CLOSED, STATE_UNKNOWN, ATTR_ENTITY_ID)
|
||||
STATE_CLOSED, STATE_UNKNOWN, STATE_OPENING, STATE_CLOSING, ATTR_ENTITY_ID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -86,24 +87,28 @@ SERVICE_TO_METHOD = {
|
||||
}
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_closed(hass, entity_id=None):
|
||||
"""Return if the cover is closed based on the statemachine."""
|
||||
entity_id = entity_id or ENTITY_ID_ALL_COVERS
|
||||
return hass.states.is_state(entity_id, STATE_CLOSED)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def open_cover(hass, entity_id=None):
|
||||
"""Open all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_OPEN_COVER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def close_cover(hass, entity_id=None):
|
||||
"""Close all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_cover_position(hass, position, entity_id=None):
|
||||
"""Move to specific position all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
@@ -111,24 +116,28 @@ def set_cover_position(hass, position, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def stop_cover(hass, entity_id=None):
|
||||
"""Stop all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_STOP_COVER, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def open_cover_tilt(hass, entity_id=None):
|
||||
"""Open all or specified cover tilt."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_OPEN_COVER_TILT, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def close_cover_tilt(hass, entity_id=None):
|
||||
"""Close all or specified cover tilt."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
hass.services.call(DOMAIN, SERVICE_CLOSE_COVER_TILT, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_cover_tilt_position(hass, tilt_position, entity_id=None):
|
||||
"""Move to specific tilt position all or specified cover."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
@@ -136,6 +145,7 @@ def set_cover_tilt_position(hass, tilt_position, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def stop_cover_tilt(hass, entity_id=None):
|
||||
"""Stop all or specified cover tilt."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
@@ -215,6 +225,11 @@ class CoverDevice(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the cover."""
|
||||
if self.is_opening:
|
||||
return STATE_OPENING
|
||||
if self.is_closing:
|
||||
return STATE_CLOSING
|
||||
|
||||
closed = self.is_closed
|
||||
|
||||
if closed is None:
|
||||
@@ -252,6 +267,16 @@ class CoverDevice(Entity):
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Return if the cover is opening or not."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return if the cover is closing or not."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed or not."""
|
||||
|
||||
@@ -112,10 +112,7 @@ class CommandCover(CoverDevice):
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.current_cover_position == 0
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
||||
@@ -35,10 +35,12 @@ class DemoCover(CoverDevice):
|
||||
self._set_position = None
|
||||
self._set_tilt_position = None
|
||||
self._tilt_position = tilt_position
|
||||
self._closing = True
|
||||
self._closing_tilt = True
|
||||
self._requested_closing = True
|
||||
self._requested_closing_tilt = True
|
||||
self._unsub_listener_cover = None
|
||||
self._unsub_listener_cover_tilt = None
|
||||
self._is_opening = False
|
||||
self._is_closing = False
|
||||
if position is None:
|
||||
self._closed = True
|
||||
else:
|
||||
@@ -69,6 +71,16 @@ class DemoCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
return self._closed
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return if the cover is closing."""
|
||||
return self._is_closing
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Return if the cover is opening."""
|
||||
return self._is_opening
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
@@ -79,8 +91,7 @@ class DemoCover(CoverDevice):
|
||||
"""Flag supported features."""
|
||||
if self._supported_features is not None:
|
||||
return self._supported_features
|
||||
else:
|
||||
return super().supported_features
|
||||
return super().supported_features
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
@@ -91,8 +102,10 @@ class DemoCover(CoverDevice):
|
||||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
self._is_closing = True
|
||||
self._listen_cover()
|
||||
self._closing = True
|
||||
self._requested_closing = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def close_cover_tilt(self, **kwargs):
|
||||
"""Close the cover tilt."""
|
||||
@@ -100,7 +113,7 @@ class DemoCover(CoverDevice):
|
||||
return
|
||||
|
||||
self._listen_cover_tilt()
|
||||
self._closing_tilt = True
|
||||
self._requested_closing_tilt = True
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
@@ -111,8 +124,10 @@ class DemoCover(CoverDevice):
|
||||
self.schedule_update_ha_state()
|
||||
return
|
||||
|
||||
self._is_opening = True
|
||||
self._listen_cover()
|
||||
self._closing = False
|
||||
self._requested_closing = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def open_cover_tilt(self, **kwargs):
|
||||
"""Open the cover tilt."""
|
||||
@@ -120,7 +135,7 @@ class DemoCover(CoverDevice):
|
||||
return
|
||||
|
||||
self._listen_cover_tilt()
|
||||
self._closing_tilt = False
|
||||
self._requested_closing_tilt = False
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
@@ -129,7 +144,7 @@ class DemoCover(CoverDevice):
|
||||
return
|
||||
|
||||
self._listen_cover()
|
||||
self._closing = position < self._position
|
||||
self._requested_closing = position < self._position
|
||||
|
||||
def set_cover_tilt_position(self, tilt_position, **kwargs):
|
||||
"""Move the cover til to a specific position."""
|
||||
@@ -138,10 +153,12 @@ class DemoCover(CoverDevice):
|
||||
return
|
||||
|
||||
self._listen_cover_tilt()
|
||||
self._closing_tilt = tilt_position < self._tilt_position
|
||||
self._requested_closing_tilt = tilt_position < self._tilt_position
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._is_closing = False
|
||||
self._is_opening = False
|
||||
if self._position is None:
|
||||
return
|
||||
if self._unsub_listener_cover is not None:
|
||||
@@ -167,7 +184,7 @@ class DemoCover(CoverDevice):
|
||||
|
||||
def _time_changed_cover(self, now):
|
||||
"""Track time changes."""
|
||||
if self._closing:
|
||||
if self._requested_closing:
|
||||
self._position -= 10
|
||||
else:
|
||||
self._position += 10
|
||||
@@ -187,7 +204,7 @@ class DemoCover(CoverDevice):
|
||||
|
||||
def _time_changed_cover_tilt(self, now):
|
||||
"""Track time changes."""
|
||||
if self._closing_tilt:
|
||||
if self._requested_closing_tilt:
|
||||
self._tilt_position -= 10
|
||||
else:
|
||||
self._tilt_position += 10
|
||||
|
||||
@@ -159,8 +159,7 @@ class GaradgetCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state == STATE_CLOSED
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
@@ -52,10 +52,7 @@ class HMCover(HMDevice, CoverDevice):
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.current_cover_position == 0
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
|
||||
@@ -23,7 +23,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Lutron Caseta Serena shades as a cover device."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade"])
|
||||
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade",
|
||||
"SerenaHoneycombShade"])
|
||||
for cover_device in cover_devices:
|
||||
dev = LutronCasetaCover(cover_device, bridge)
|
||||
devs.append(dev)
|
||||
|
||||
@@ -361,8 +361,7 @@ class MqttCover(CoverDevice):
|
||||
position_percentage = float(offset_position) / tilt_range * 100.0
|
||||
if self._tilt_invert:
|
||||
return 100 - position_percentage
|
||||
else:
|
||||
return position_percentage
|
||||
return position_percentage
|
||||
|
||||
def find_in_range_from_percent(self, percentage):
|
||||
"""
|
||||
|
||||
@@ -12,7 +12,6 @@ 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 = ['pymyq==0.0.8']
|
||||
|
||||
@@ -37,7 +36,6 @@ 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)
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
myq = pymyq(username, password, brand)
|
||||
|
||||
try:
|
||||
@@ -52,8 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
|
||||
@@ -53,8 +53,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_DIMMER in self._values:
|
||||
return self._values.get(set_req.V_DIMMER) == 0
|
||||
else:
|
||||
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
||||
@@ -117,8 +117,7 @@ class OpenGarageCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state in [STATE_CLOSED, STATE_OPENING]
|
||||
return self._state in [STATE_CLOSED, STATE_OPENING]
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the cover."""
|
||||
|
||||
@@ -40,14 +40,15 @@ STOP_ACTION = 'stop_cover'
|
||||
POSITION_ACTION = 'set_cover_position'
|
||||
TILT_ACTION = 'set_cover_tilt_position'
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position'
|
||||
CONF_OPEN_OR_CLOSE = 'open_or_close'
|
||||
|
||||
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(OPEN_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CLOSE_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(STOP_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(OPEN_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(CLOSE_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Exclusive(CONF_POSITION_TEMPLATE,
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
|
||||
vol.Exclusive(CONF_VALUE_TEMPLATE,
|
||||
@@ -77,9 +78,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
position_template = device_config.get(CONF_POSITION_TEMPLATE)
|
||||
tilt_template = device_config.get(CONF_TILT_TEMPLATE)
|
||||
icon_template = device_config.get(CONF_ICON_TEMPLATE)
|
||||
open_action = device_config[OPEN_ACTION]
|
||||
close_action = device_config[CLOSE_ACTION]
|
||||
stop_action = device_config[STOP_ACTION]
|
||||
open_action = device_config.get(OPEN_ACTION)
|
||||
close_action = device_config.get(CLOSE_ACTION)
|
||||
stop_action = device_config.get(STOP_ACTION)
|
||||
position_action = device_config.get(POSITION_ACTION)
|
||||
tilt_action = device_config.get(TILT_ACTION)
|
||||
|
||||
@@ -88,6 +89,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE)
|
||||
continue
|
||||
|
||||
if position_action is None and open_action is None:
|
||||
_LOGGER.error('Must specify at least one of %s' or '%s',
|
||||
OPEN_ACTION, POSITION_ACTION)
|
||||
continue
|
||||
template_entity_ids = set()
|
||||
if state_template is not None:
|
||||
temp_ids = state_template.extract_entities()
|
||||
@@ -147,9 +152,15 @@ class CoverTemplate(CoverDevice):
|
||||
self._position_template = position_template
|
||||
self._tilt_template = tilt_template
|
||||
self._icon_template = icon_template
|
||||
self._open_script = Script(hass, open_action)
|
||||
self._close_script = Script(hass, close_action)
|
||||
self._stop_script = Script(hass, stop_action)
|
||||
self._open_script = None
|
||||
if open_action is not None:
|
||||
self._open_script = Script(hass, open_action)
|
||||
self._close_script = None
|
||||
if close_action is not None:
|
||||
self._close_script = Script(hass, close_action)
|
||||
self._stop_script = None
|
||||
if stop_action is not None:
|
||||
self._stop_script = Script(hass, stop_action)
|
||||
self._position_script = None
|
||||
if position_action is not None:
|
||||
self._position_script = Script(hass, position_action)
|
||||
@@ -227,9 +238,12 @@ class CoverTemplate(CoverDevice):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
if self.current_cover_position is not None:
|
||||
if self._stop_script is not None:
|
||||
supported_features |= SUPPORT_STOP
|
||||
|
||||
if self._position_script is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
if self.current_cover_tilt_position is not None:
|
||||
@@ -245,23 +259,30 @@ class CoverTemplate(CoverDevice):
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
self.hass.async_add_job(self._open_script.async_run())
|
||||
if self._open_script:
|
||||
self.hass.async_add_job(self._open_script.async_run())
|
||||
elif self._position_script:
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": 100}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
self.hass.async_add_job(self._close_script.async_run())
|
||||
if self._close_script:
|
||||
self.hass.async_add_job(self._close_script.async_run())
|
||||
elif self._position_script:
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": 0}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
self.hass.async_add_job(self._stop_script.async_run())
|
||||
if self._stop_script:
|
||||
self.hass.async_add_job(self._stop_script.async_run())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Set cover position."""
|
||||
if ATTR_POSITION not in kwargs:
|
||||
return
|
||||
self._position = kwargs[ATTR_POSITION]
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": self._position}))
|
||||
@@ -283,8 +304,6 @@ class CoverTemplate(CoverDevice):
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if ATTR_TILT_POSITION not in kwargs:
|
||||
return
|
||||
self._tilt_value = kwargs[ATTR_TILT_POSITION]
|
||||
self.hass.async_add_job(self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}))
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Support for Velbus covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.velbus/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE,
|
||||
SUPPORT_STOP)
|
||||
from homeassistant.components.velbus import DOMAIN
|
||||
from homeassistant.const import (CONF_COVERS, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required('module'): cv.positive_int,
|
||||
vol.Required('open_channel'): cv.positive_int,
|
||||
vol.Required('close_channel'): cv.positive_int,
|
||||
vol.Required(CONF_NAME): cv.string
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
|
||||
})
|
||||
|
||||
DEPENDENCIES = ['velbus']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up cover controlled by Velbus."""
|
||||
devices = config.get(CONF_COVERS, {})
|
||||
covers = []
|
||||
|
||||
velbus = hass.data[DOMAIN]
|
||||
for device_name, device_config in devices.items():
|
||||
covers.append(
|
||||
VelbusCover(
|
||||
velbus,
|
||||
device_config.get(CONF_NAME, device_name),
|
||||
device_config.get('module'),
|
||||
device_config.get('open_channel'),
|
||||
device_config.get('close_channel')
|
||||
)
|
||||
)
|
||||
|
||||
if not covers:
|
||||
_LOGGER.error("No covers added")
|
||||
return False
|
||||
|
||||
add_devices(covers)
|
||||
|
||||
|
||||
class VelbusCover(CoverDevice):
|
||||
"""Representation a Velbus cover."""
|
||||
|
||||
def __init__(self, velbus, name, module, open_channel, close_channel):
|
||||
"""Initialize the cover."""
|
||||
self._velbus = velbus
|
||||
self._name = name
|
||||
self._close_channel_state = None
|
||||
self._open_channel_state = None
|
||||
self._module = module
|
||||
self._open_channel = open_channel
|
||||
self._close_channel = close_channel
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add listener for Velbus messages on bus."""
|
||||
def _init_velbus():
|
||||
"""Initialize Velbus on startup."""
|
||||
self._velbus.subscribe(self._on_message)
|
||||
self.get_status()
|
||||
|
||||
yield from self.hass.async_add_job(_init_velbus)
|
||||
|
||||
def _on_message(self, message):
|
||||
import velbus
|
||||
if isinstance(message, velbus.RelayStatusMessage):
|
||||
if message.address == self._module:
|
||||
if message.channel == self._close_channel:
|
||||
self._close_channel_state = message.is_on()
|
||||
self.schedule_update_ha_state()
|
||||
if message.channel == self._open_channel:
|
||||
self._open_channel_state = message.is_on()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._close_channel_state
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
|
||||
None is unknown.
|
||||
"""
|
||||
return None
|
||||
|
||||
def _relay_off(self, channel):
|
||||
import velbus
|
||||
message = velbus.SwitchRelayOffMessage()
|
||||
message.set_defaults(self._module)
|
||||
message.relay_channels = [channel]
|
||||
self._velbus.send(message)
|
||||
|
||||
def _relay_on(self, channel):
|
||||
import velbus
|
||||
message = velbus.SwitchRelayOnMessage()
|
||||
message.set_defaults(self._module)
|
||||
message.relay_channels = [channel]
|
||||
self._velbus.send(message)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._relay_off(self._close_channel)
|
||||
time.sleep(0.3)
|
||||
self._relay_on(self._open_channel)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._relay_off(self._open_channel)
|
||||
time.sleep(0.3)
|
||||
self._relay_on(self._close_channel)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._relay_off(self._open_channel)
|
||||
time.sleep(0.3)
|
||||
self._relay_off(self._close_channel)
|
||||
|
||||
def get_status(self):
|
||||
"""Retrieve current status."""
|
||||
import velbus
|
||||
message = velbus.ModuleStatusRequestMessage()
|
||||
message.set_defaults(self._module)
|
||||
message.channels = [self._open_channel, self._close_channel]
|
||||
self._velbus.send(message)
|
||||
@@ -53,10 +53,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.current_cover_position == 0
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
|
||||
@@ -29,20 +29,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""Representation of a Wink cover device."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the cover."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['cover'].append(self)
|
||||
|
||||
def close_cover(self):
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the shade."""
|
||||
self.wink.set_state(0)
|
||||
|
||||
def open_cover(self):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the shade."""
|
||||
self.wink.set_state(1)
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Support for Xiaomi curtain."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.xiaomi import (PY_XIAOMI_GATEWAY, XiaomiDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CURTAIN_LEVEL = 'curtain_level'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Perform the setup for Xiaomi devices."""
|
||||
devices = []
|
||||
for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items():
|
||||
for device in gateway.devices['cover']:
|
||||
model = device['model']
|
||||
if model == 'curtain':
|
||||
devices.append(XiaomiGenericCover(device, "Curtain",
|
||||
{'status': 'status',
|
||||
'pos': 'curtain_level'},
|
||||
gateway))
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class XiaomiGenericCover(XiaomiDevice, CoverDevice):
|
||||
"""Representation of a XiaomiPlug."""
|
||||
|
||||
def __init__(self, device, name, data_key, xiaomi_hub):
|
||||
"""Initialize the XiaomiPlug."""
|
||||
self._data_key = data_key
|
||||
self._pos = 0
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of the cover."""
|
||||
return self._pos
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self.current_cover_position < 0
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'close')
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'open')
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'stop')
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
self._write_to_hub(self._sid, self._data_key['pos'], str(position))
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
if ATTR_CURTAIN_LEVEL in data:
|
||||
self._pos = int(data[ATTR_CURTAIN_LEVEL])
|
||||
return True
|
||||
return False
|
||||
@@ -73,8 +73,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
return None
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
@@ -86,8 +85,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
return 0
|
||||
elif self._current_position >= 95:
|
||||
return 100
|
||||
else:
|
||||
return self._current_position
|
||||
return self._current_position
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
@@ -112,24 +110,36 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def __init__(self, values):
|
||||
"""Initialize the zwave garage door."""
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._state = None
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Handle data changes for node values."""
|
||||
self._state = self.values.primary.data
|
||||
_LOGGER.debug("self._state=%s", self._state)
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Return true if cover is in an opening state."""
|
||||
return self._state == "Opening"
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return true if cover is in an closing state."""
|
||||
return self._state == "Closing"
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of Zwave garage door."""
|
||||
return not self._state
|
||||
return self._state == "Closed"
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the garage door."""
|
||||
self.values.primary.data = False
|
||||
self.values.primary.data = "Closed"
|
||||
|
||||
def open_cover(self):
|
||||
"""Open the garage door."""
|
||||
self.values.primary.data = True
|
||||
self.values.primary.data = "Opened"
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
||||
@@ -9,7 +9,6 @@ import time
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
|
||||
DEPENDENCIES = ['conversation', 'introduction', 'zone']
|
||||
@@ -38,9 +37,9 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the demo environment."""
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
group = hass.components.group
|
||||
configurator = hass.components.configurator
|
||||
persistent_notification = hass.components.persistent_notification
|
||||
|
||||
config.setdefault(ha.DOMAIN, {})
|
||||
config.setdefault(DOMAIN, {})
|
||||
@@ -108,7 +107,7 @@ def async_setup(hass, config):
|
||||
|
||||
# Set up example persistent notification
|
||||
persistent_notification.async_create(
|
||||
hass, 'This is an example of a persistent notification.',
|
||||
'This is an example of a persistent notification.',
|
||||
title='Example Notification')
|
||||
|
||||
# Set up room groups
|
||||
@@ -206,7 +205,7 @@ def async_setup(hass, config):
|
||||
def setup_configurator():
|
||||
"""Set up a configurator."""
|
||||
request_id = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
"Philips Hue", hue_configuration_callback,
|
||||
description=("Press the button on the bridge to register Philips "
|
||||
"Hue with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
|
||||
@@ -16,6 +16,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||
@@ -93,6 +94,7 @@ DISCOVERY_PLATFORMS = {
|
||||
}
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistantType, entity_id: str=None):
|
||||
"""Return the state if any or a specified device is home."""
|
||||
entity = entity_id or ENTITY_ID_ALL_DEVICES
|
||||
|
||||
@@ -7,9 +7,7 @@ https://home-assistant.io/components/device_tracker.actiontec/
|
||||
import logging
|
||||
import re
|
||||
import telnetlib
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -17,9 +15,6 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,7 +49,6 @@ class ActiontecDeviceScanner(DeviceScanner):
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.lock = threading.Lock()
|
||||
self.last_results = []
|
||||
data = self.get_actiontec_data()
|
||||
self.success_init = data is not None
|
||||
@@ -74,7 +68,6 @@ class ActiontecDeviceScanner(DeviceScanner):
|
||||
return client.ip
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the router is up to date.
|
||||
|
||||
@@ -84,16 +77,15 @@ class ActiontecDeviceScanner(DeviceScanner):
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
now = dt_util.now()
|
||||
actiontec_data = self.get_actiontec_data()
|
||||
if not actiontec_data:
|
||||
return False
|
||||
self.last_results = [Device(data['mac'], name, now)
|
||||
for name, data in actiontec_data.items()
|
||||
if data['timevalid'] > -60]
|
||||
_LOGGER.info("Scan successful")
|
||||
return True
|
||||
now = dt_util.now()
|
||||
actiontec_data = self.get_actiontec_data()
|
||||
if not actiontec_data:
|
||||
return False
|
||||
self.last_results = [Device(data['mac'], name, now)
|
||||
for name, data in actiontec_data.items()
|
||||
if data['timevalid'] > -60]
|
||||
_LOGGER.info("Scan successful")
|
||||
return True
|
||||
|
||||
def get_actiontec_data(self):
|
||||
"""Retrieve data from Actiontec MI424WR and return parsed result."""
|
||||
|
||||
@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.aruba/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -15,14 +13,11 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_DEVICES_REGEX = re.compile(
|
||||
r'(?P<name>([^\s]+))\s+' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' +
|
||||
@@ -52,8 +47,6 @@ class ArubaDeviceScanner(DeviceScanner):
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
# Test the router is accessible.
|
||||
@@ -74,7 +67,6 @@ class ArubaDeviceScanner(DeviceScanner):
|
||||
return client['name']
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the Aruba Access Point is up to date.
|
||||
|
||||
@@ -83,13 +75,12 @@ class ArubaDeviceScanner(DeviceScanner):
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
data = self.get_aruba_data()
|
||||
if not data:
|
||||
return False
|
||||
data = self.get_aruba_data()
|
||||
if not data:
|
||||
return False
|
||||
|
||||
self.last_results = data.values()
|
||||
return True
|
||||
self.last_results = data.values()
|
||||
return True
|
||||
|
||||
def get_aruba_data(self):
|
||||
"""Retrieve data from Aruba Access Point and return parsed result."""
|
||||
|
||||
@@ -8,9 +8,7 @@ import logging
|
||||
import re
|
||||
import socket
|
||||
import telnetlib
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -18,7 +16,6 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
@@ -32,8 +29,6 @@ CONF_SSH_KEY = 'ssh_key'
|
||||
|
||||
DEFAULT_SSH_PORT = 22
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
SECRET_GROUP = 'Password or SSH Key'
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
@@ -60,20 +55,11 @@ _LEASES_REGEX = re.compile(
|
||||
r'(?P<host>([^\s]+))')
|
||||
|
||||
# Command to get both 5GHz and 2.4GHz clients
|
||||
_WL_CMD = '{ wl -i eth2 assoclist & wl -i eth1 assoclist ; }'
|
||||
_WL_CMD = 'for dev in `nvram get wl_ifnames`; do wl -i $dev assoclist; done'
|
||||
_WL_REGEX = re.compile(
|
||||
r'\w+\s' +
|
||||
r'(?P<mac>(([0-9A-F]{2}[:-]){5}([0-9A-F]{2})))')
|
||||
|
||||
_ARP_CMD = 'arp -n'
|
||||
_ARP_REGEX = re.compile(
|
||||
r'.+\s' +
|
||||
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
|
||||
r'.+\s' +
|
||||
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
|
||||
r'\s' +
|
||||
r'.*')
|
||||
|
||||
_IP_NEIGH_CMD = 'ip neigh'
|
||||
_IP_NEIGH_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3}|'
|
||||
@@ -84,15 +70,6 @@ _IP_NEIGH_REGEX = re.compile(
|
||||
r'\s?(router)?'
|
||||
r'(?P<status>(\w+))')
|
||||
|
||||
_NVRAM_CMD = 'nvram get client_info_tmp'
|
||||
_NVRAM_REGEX = re.compile(
|
||||
r'.*>.*>' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||
r'>' +
|
||||
r'(?P<mac>(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' +
|
||||
r'>' +
|
||||
r'.*')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
@@ -102,7 +79,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases')
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(DeviceScanner):
|
||||
@@ -141,8 +118,6 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
self.password,
|
||||
self.mode == "ap")
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
# Test the router is accessible.
|
||||
@@ -163,7 +138,6 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
return client['host']
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the ASUSWRT router is up to date.
|
||||
|
||||
@@ -172,19 +146,18 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info('Checking ARP')
|
||||
data = self.get_asuswrt_data()
|
||||
if not data:
|
||||
return False
|
||||
_LOGGER.info('Checking Devices')
|
||||
data = self.get_asuswrt_data()
|
||||
if not data:
|
||||
return False
|
||||
|
||||
active_clients = [client for client in data.values() if
|
||||
client['status'] == 'REACHABLE' or
|
||||
client['status'] == 'DELAY' or
|
||||
client['status'] == 'STALE' or
|
||||
client['status'] == 'IN_NVRAM']
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
active_clients = [client for client in data.values() if
|
||||
client['status'] == 'REACHABLE' or
|
||||
client['status'] == 'DELAY' or
|
||||
client['status'] == 'STALE' or
|
||||
client['status'] == 'IN_ASSOCLIST']
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
def get_asuswrt_data(self):
|
||||
"""Retrieve data from ASUSWRT and return parsed result."""
|
||||
@@ -204,41 +177,12 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
|
||||
host = ''
|
||||
|
||||
# match mac addresses to IP addresses in ARP table
|
||||
for arp in result.arp:
|
||||
if match.group('mac').lower() in \
|
||||
arp.decode('utf-8').lower():
|
||||
arp_match = _ARP_REGEX.search(
|
||||
arp.decode('utf-8').lower())
|
||||
if not arp_match:
|
||||
_LOGGER.warning("Could not parse arp row: %s", arp)
|
||||
continue
|
||||
|
||||
devices[arp_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': arp_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
# match mac addresses to IP addresses in NVRAM table
|
||||
for nvr in result.nvram:
|
||||
if match.group('mac').upper() in nvr.decode('utf-8'):
|
||||
nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8'))
|
||||
if not nvram_match:
|
||||
_LOGGER.warning("Could not parse nvr row: %s", nvr)
|
||||
continue
|
||||
|
||||
# skip current check if already in ARP table
|
||||
if nvram_match.group('ip') in devices.keys():
|
||||
continue
|
||||
|
||||
devices[nvram_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': 'IN_NVRAM',
|
||||
'ip': nvram_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
devices[match.group('mac').upper()] = {
|
||||
'host': host,
|
||||
'status': 'IN_ASSOCLIST',
|
||||
'ip': '',
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
else:
|
||||
for lease in result.leases:
|
||||
@@ -256,20 +200,23 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
devices[match.group('mac')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
for neighbor in result.neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
|
||||
continue
|
||||
if match.group('ip') in devices:
|
||||
devices[match.group('ip')]['status'] = match.group('status')
|
||||
for neighbor in result.neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s",
|
||||
neighbor)
|
||||
continue
|
||||
if match.group('mac') in devices:
|
||||
devices[match.group('mac')]['status'] = (
|
||||
match.group('status'))
|
||||
|
||||
return devices
|
||||
|
||||
|
||||
@@ -317,27 +264,19 @@ class SshConnection(_Connection):
|
||||
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]
|
||||
neighbors = ['']
|
||||
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(_IP_NEIGH_CMD)
|
||||
self._ssh.prompt()
|
||||
neighbors = self._ssh.before.split(b'\n')[1:-1]
|
||||
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)
|
||||
return AsusWrtResult(neighbors, leases_result)
|
||||
except exceptions.EOF as err:
|
||||
_LOGGER.error("Connection refused. SSH enabled?")
|
||||
self.disconnect()
|
||||
@@ -407,23 +346,14 @@ class TelnetConnection(_Connection):
|
||||
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)
|
||||
return AsusWrtResult(neighbors, leases_result)
|
||||
except EOFError:
|
||||
_LOGGER.error("Unexpected response from router")
|
||||
self.disconnect()
|
||||
|
||||
@@ -52,8 +52,7 @@ class BboxDeviceScanner(DeviceScanner):
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
||||
@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.bt_home_hub_5/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
from urllib.parse import unquote
|
||||
@@ -19,13 +17,10 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string
|
||||
})
|
||||
@@ -46,11 +41,7 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
|
||||
"""Initialise the scanner."""
|
||||
_LOGGER.info("Initialising BT Home Hub 5")
|
||||
self.host = config.get(CONF_HOST, '192.168.1.254')
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
|
||||
|
||||
# Test the router is accessible
|
||||
@@ -65,17 +56,15 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
# If not initialised and not already scanned and not found.
|
||||
if device not in self.last_results:
|
||||
self._update_info()
|
||||
# If not initialised and not already scanned and not found.
|
||||
if device not in self.last_results:
|
||||
self._update_info()
|
||||
|
||||
if not self.last_results:
|
||||
return None
|
||||
if not self.last_results:
|
||||
return None
|
||||
|
||||
return self.last_results.get(device)
|
||||
return self.last_results.get(device)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the BT Home Hub 5 is up to date.
|
||||
|
||||
@@ -84,18 +73,17 @@ class BTHomeHub5DeviceScanner(DeviceScanner):
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Scanning")
|
||||
_LOGGER.info("Scanning")
|
||||
|
||||
data = _get_homehub_data(self.url)
|
||||
data = _get_homehub_data(self.url)
|
||||
|
||||
if not data:
|
||||
_LOGGER.warning("Error scanning devices")
|
||||
return False
|
||||
if not data:
|
||||
_LOGGER.warning("Error scanning devices")
|
||||
return False
|
||||
|
||||
self.last_results = data
|
||||
self.last_results = data
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def _get_homehub_data(url):
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.cisco_ios/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -14,9 +13,6 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
|
||||
CONF_PORT
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -65,7 +61,6 @@ class CiscoDeviceScanner(DeviceScanner):
|
||||
|
||||
return self.last_results
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensure the information from the Cisco router is up to date.
|
||||
@@ -87,21 +82,20 @@ class CiscoDeviceScanner(DeviceScanner):
|
||||
lines_result = lines_result[2:]
|
||||
|
||||
for line in lines_result:
|
||||
if len(line.split()) is 6:
|
||||
parts = line.split()
|
||||
if len(parts) != 6:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) != 6:
|
||||
continue
|
||||
|
||||
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
|
||||
# 'GigabitEthernet0']
|
||||
age = parts[2]
|
||||
hw_addr = parts[3]
|
||||
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
|
||||
# 'GigabitEthernet0']
|
||||
age = parts[2]
|
||||
hw_addr = parts[3]
|
||||
|
||||
if age != "-":
|
||||
mac = _parse_cisco_mac_address(hw_addr)
|
||||
age = int(age)
|
||||
if age < 1:
|
||||
last_results.append(mac)
|
||||
if age != "-":
|
||||
mac = _parse_cisco_mac_address(hw_addr)
|
||||
age = int(age)
|
||||
if age < 1:
|
||||
last_results.append(mac)
|
||||
|
||||
self.last_results = last_results
|
||||
return True
|
||||
|
||||
@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.ddwrt/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@@ -16,9 +14,6 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,8 +45,6 @@ class DdWrtDeviceScanner(DeviceScanner):
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
self.mac2name = {}
|
||||
|
||||
@@ -69,68 +62,65 @@ class DdWrtDeviceScanner(DeviceScanner):
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
# If not initialised and not already scanned and not found.
|
||||
if device not in self.mac2name:
|
||||
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
# If not initialised and not already scanned and not found.
|
||||
if device not in self.mac2name:
|
||||
url = 'http://{}/Status_Lan.live.asp'.format(self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
if not data:
|
||||
return None
|
||||
|
||||
dhcp_leases = data.get('dhcp_leases', None)
|
||||
dhcp_leases = data.get('dhcp_leases', None)
|
||||
|
||||
if not dhcp_leases:
|
||||
return None
|
||||
if not dhcp_leases:
|
||||
return None
|
||||
|
||||
# Remove leading and trailing quotes and spaces
|
||||
cleaned_str = dhcp_leases.replace(
|
||||
"\"", "").replace("\'", "").replace(" ", "")
|
||||
elements = cleaned_str.split(',')
|
||||
num_clients = int(len(elements) / 5)
|
||||
self.mac2name = {}
|
||||
for idx in range(0, num_clients):
|
||||
# The data is a single array
|
||||
# every 5 elements represents one host, the MAC
|
||||
# is the third element and the name is the first.
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
mac = elements[mac_index]
|
||||
self.mac2name[mac] = elements[idx * 5]
|
||||
# Remove leading and trailing quotes and spaces
|
||||
cleaned_str = dhcp_leases.replace(
|
||||
"\"", "").replace("\'", "").replace(" ", "")
|
||||
elements = cleaned_str.split(',')
|
||||
num_clients = int(len(elements) / 5)
|
||||
self.mac2name = {}
|
||||
for idx in range(0, num_clients):
|
||||
# The data is a single array
|
||||
# every 5 elements represents one host, the MAC
|
||||
# is the third element and the name is the first.
|
||||
mac_index = (idx * 5) + 2
|
||||
if mac_index < len(elements):
|
||||
mac = elements[mac_index]
|
||||
self.mac2name[mac] = elements[idx * 5]
|
||||
|
||||
return self.mac2name.get(device)
|
||||
return self.mac2name.get(device)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the DD-WRT router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
with self.lock:
|
||||
_LOGGER.info("Checking ARP")
|
||||
_LOGGER.info("Checking ARP")
|
||||
|
||||
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
|
||||
data = self.get_ddwrt_data(url)
|
||||
|
||||
if not data:
|
||||
return False
|
||||
if not data:
|
||||
return False
|
||||
|
||||
self.last_results = []
|
||||
self.last_results = []
|
||||
|
||||
active_clients = data.get('active_wireless', None)
|
||||
if not active_clients:
|
||||
return False
|
||||
active_clients = data.get('active_wireless', None)
|
||||
if not active_clients:
|
||||
return False
|
||||
|
||||
# The DD-WRT UI uses its own data format and then
|
||||
# regex's out values so this is done here too
|
||||
# Remove leading and trailing single quotes.
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
# The DD-WRT UI uses its own data format and then
|
||||
# regex's out values so this is done here too
|
||||
# Remove leading and trailing single quotes.
|
||||
clean_str = active_clients.strip().strip("'")
|
||||
elements = clean_str.split("','")
|
||||
|
||||
self.last_results.extend(item for item in elements
|
||||
if _MAC_REGEX.match(item))
|
||||
self.last_results.extend(item for item in elements
|
||||
if _MAC_REGEX.match(item))
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
def get_ddwrt_data(self, url):
|
||||
"""Retrieve data from DD-WRT and return parsed result."""
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.fritz/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -13,12 +12,9 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DEFAULT_IP = '169.254.1.1' # This IP is valid for all FRITZ!Box routers.
|
||||
@@ -88,7 +84,6 @@ class FritzBoxScanner(DeviceScanner):
|
||||
return None
|
||||
return ret
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Retrieve latest information from the FRITZ!Box."""
|
||||
if not self.success_init:
|
||||
|
||||
@@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.linksys_ap/
|
||||
"""
|
||||
import base64
|
||||
import logging
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@@ -16,9 +14,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL)
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
INTERFACES = 2
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
@@ -51,8 +47,6 @@ class LinksysAPDeviceScanner(object):
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.verify_ssl = config[CONF_VERIFY_SSL]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
self.last_results = []
|
||||
|
||||
# Check if the access point is accessible
|
||||
@@ -76,24 +70,22 @@ class LinksysAPDeviceScanner(object):
|
||||
"""
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Check for connected devices."""
|
||||
from bs4 import BeautifulSoup as BS
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Checking Linksys AP")
|
||||
_LOGGER.info("Checking Linksys AP")
|
||||
|
||||
self.last_results = []
|
||||
for interface in range(INTERFACES):
|
||||
request = self._make_request(interface)
|
||||
self.last_results.extend(
|
||||
[x.find_all('td')[1].text
|
||||
for x in BS(request.content, "html.parser")
|
||||
.find_all(class_='section-row')]
|
||||
)
|
||||
self.last_results = []
|
||||
for interface in range(INTERFACES):
|
||||
request = self._make_request(interface)
|
||||
self.last_results.extend(
|
||||
[x.find_all('td')[1].text
|
||||
for x in BS(request.content, "html.parser")
|
||||
.find_all(class_='section-row')]
|
||||
)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
def _make_request(self, unit=0):
|
||||
# No, the '&&' is not a typo - this is expected by the web interface.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Support for Linksys Smart Wifi routers."""
|
||||
import logging
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@@ -10,9 +8,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -36,8 +32,6 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner):
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
self.last_results = {}
|
||||
|
||||
# Check if the access point is accessible
|
||||
@@ -55,45 +49,46 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner):
|
||||
"""Return the name (if known) of the device."""
|
||||
return self.last_results.get(mac)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Check for connected devices."""
|
||||
with self.lock:
|
||||
_LOGGER.info("Checking Linksys Smart Wifi")
|
||||
_LOGGER.info("Checking Linksys Smart Wifi")
|
||||
|
||||
self.last_results = {}
|
||||
response = self._make_request()
|
||||
if response.status_code != 200:
|
||||
_LOGGER.error(
|
||||
"Got HTTP status code %d when getting device list",
|
||||
response.status_code)
|
||||
return False
|
||||
try:
|
||||
data = response.json()
|
||||
result = data["responses"][0]
|
||||
devices = result["output"]["devices"]
|
||||
for device in devices:
|
||||
macs = device["knownMACAddresses"]
|
||||
if not macs:
|
||||
_LOGGER.warning(
|
||||
"Skipping device without known MAC address")
|
||||
continue
|
||||
mac = macs[-1]
|
||||
connections = device["connections"]
|
||||
if not connections:
|
||||
_LOGGER.debug("Device %s is not connected", mac)
|
||||
continue
|
||||
name = device["friendlyName"]
|
||||
properties = device["properties"]
|
||||
for prop in properties:
|
||||
if prop["name"] == "userDeviceName":
|
||||
name = prop["value"]
|
||||
_LOGGER.debug("Device %s is connected", mac)
|
||||
self.last_results[mac] = name
|
||||
except (KeyError, IndexError):
|
||||
_LOGGER.exception("Router returned unexpected response")
|
||||
return False
|
||||
return True
|
||||
self.last_results = {}
|
||||
response = self._make_request()
|
||||
if response.status_code != 200:
|
||||
_LOGGER.error(
|
||||
"Got HTTP status code %d when getting device list",
|
||||
response.status_code)
|
||||
return False
|
||||
try:
|
||||
data = response.json()
|
||||
result = data["responses"][0]
|
||||
devices = result["output"]["devices"]
|
||||
for device in devices:
|
||||
macs = device["knownMACAddresses"]
|
||||
if not macs:
|
||||
_LOGGER.warning(
|
||||
"Skipping device without known MAC address")
|
||||
continue
|
||||
mac = macs[-1]
|
||||
connections = device["connections"]
|
||||
if not connections:
|
||||
_LOGGER.debug("Device %s is not connected", mac)
|
||||
continue
|
||||
|
||||
name = None
|
||||
for prop in device["properties"]:
|
||||
if prop["name"] == "userDeviceName":
|
||||
name = prop["value"]
|
||||
if not name:
|
||||
name = device.get("friendlyName", device["deviceID"])
|
||||
|
||||
_LOGGER.debug("Device %s is connected", mac)
|
||||
self.last_results[mac] = name
|
||||
except (KeyError, IndexError):
|
||||
_LOGGER.exception("Router returned unexpected response")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _make_request(self):
|
||||
# Weirdly enough, this doesn't seem to require authentication
|
||||
|
||||
@@ -94,21 +94,20 @@ class LocativeView(HomeAssistantView):
|
||||
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
|
||||
# aren't currently in. This occurs when a zone is entered
|
||||
# before the previous zone was exited. The enter message will
|
||||
# be sent first, then the exit message will be sent second.
|
||||
return 'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state)
|
||||
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered
|
||||
# before the previous zone was exited. The enter message will
|
||||
# be sent first, then the exit message will be sent second.
|
||||
return 'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state)
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
return 'Received test message.'
|
||||
|
||||
else:
|
||||
_LOGGER.error('Received unidentified message from Locative: %s',
|
||||
direction)
|
||||
return ('Received unidentified message: {}'.format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error('Received unidentified message from Locative: %s',
|
||||
direction)
|
||||
return ('Received unidentified message: {}'.format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
@@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.luci/
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@@ -18,9 +16,6 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,12 +50,8 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.refresh_token()
|
||||
|
||||
self.mac2name = None
|
||||
self.success_init = self.token is not None
|
||||
|
||||
@@ -75,24 +66,22 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
if self.mac2name is None:
|
||||
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
|
||||
result = _req_json_rpc(url, 'get_all', 'dhcp',
|
||||
params={'auth': self.token})
|
||||
if result:
|
||||
hosts = [x for x in result.values()
|
||||
if x['.type'] == 'host' and
|
||||
'mac' in x and 'name' in x]
|
||||
mac2name_list = [
|
||||
(x['mac'].upper(), x['name']) for x in hosts]
|
||||
self.mac2name = dict(mac2name_list)
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
return self.mac2name.get(device.upper(), None)
|
||||
if self.mac2name is None:
|
||||
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
|
||||
result = _req_json_rpc(url, 'get_all', 'dhcp',
|
||||
params={'auth': self.token})
|
||||
if result:
|
||||
hosts = [x for x in result.values()
|
||||
if x['.type'] == 'host' and
|
||||
'mac' in x and 'name' in x]
|
||||
mac2name_list = [
|
||||
(x['mac'].upper(), x['name']) for x in hosts]
|
||||
self.mac2name = dict(mac2name_list)
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
return self.mac2name.get(device.upper(), None)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the Luci router is up to date.
|
||||
|
||||
@@ -101,31 +90,30 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Checking ARP")
|
||||
_LOGGER.info("Checking ARP")
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||
|
||||
try:
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
except InvalidLuciTokenError:
|
||||
_LOGGER.info("Refreshing token")
|
||||
self.refresh_token()
|
||||
return False
|
||||
|
||||
if result:
|
||||
self.last_results = []
|
||||
for device_entry in result:
|
||||
# Check if the Flags for each device contain
|
||||
# NUD_REACHABLE and if so, add it to last_results
|
||||
if int(device_entry['Flags'], 16) & 0x2:
|
||||
self.last_results.append(device_entry['HW address'])
|
||||
|
||||
return True
|
||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||
|
||||
try:
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
except InvalidLuciTokenError:
|
||||
_LOGGER.info("Refreshing token")
|
||||
self.refresh_token()
|
||||
return False
|
||||
|
||||
if result:
|
||||
self.last_results = []
|
||||
for device_entry in result:
|
||||
# Check if the Flags for each device contain
|
||||
# NUD_REACHABLE and if so, add it to last_results
|
||||
if int(device_entry['Flags'], 16) & 0x2:
|
||||
self.last_results.append(device_entry['HW address'])
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _req_json_rpc(url, method, *args, **kwargs):
|
||||
"""Perform one JSON RPC operation."""
|
||||
|
||||
@@ -5,25 +5,17 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.mikrotik/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
CONF_PORT)
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||
|
||||
REQUIREMENTS = ['librouteros==1.0.2']
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
MTK_DEFAULT_API_PORT = '8728'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -54,12 +46,9 @@ class MikrotikScanner(DeviceScanner):
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.connected = False
|
||||
self.success_init = False
|
||||
self.client = None
|
||||
|
||||
self.wireless_exist = None
|
||||
self.success_init = self.connect_to_device()
|
||||
|
||||
@@ -118,51 +107,48 @@ class MikrotikScanner(DeviceScanner):
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
with self.lock:
|
||||
return self.last_results.get(mac)
|
||||
return self.last_results.get(mac)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Retrieve latest information from the Mikrotik box."""
|
||||
with self.lock:
|
||||
if self.wireless_exist:
|
||||
devices_tracker = 'wireless'
|
||||
else:
|
||||
devices_tracker = 'ip'
|
||||
if self.wireless_exist:
|
||||
devices_tracker = 'wireless'
|
||||
else:
|
||||
devices_tracker = 'ip'
|
||||
|
||||
_LOGGER.info(
|
||||
"Loading %s devices from Mikrotik (%s) ...",
|
||||
devices_tracker,
|
||||
self.host
|
||||
_LOGGER.info(
|
||||
"Loading %s devices from Mikrotik (%s) ...",
|
||||
devices_tracker,
|
||||
self.host
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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')}
|
||||
|
||||
mac_names = {device.get('mac-address'): device.get('host-name')
|
||||
for device in device_names
|
||||
if device.get('mac-address')}
|
||||
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 = {
|
||||
device.get('mac-address'):
|
||||
mac_names.get(device.get('mac-address'))
|
||||
for device in device_names
|
||||
if device.get('active-address')
|
||||
}
|
||||
|
||||
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 = {
|
||||
device.get('mac-address'):
|
||||
mac_names.get(device.get('mac-address'))
|
||||
for device in device_names
|
||||
if device.get('active-address')
|
||||
}
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user