Compare commits
236 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37bd93a975 | |||
| b78765a41f | |||
| ab60b32326 | |||
| 3ea179cc0b | |||
| 5bedf5d604 | |||
| d8c1959715 | |||
| 0f1c4d2f8c | |||
| 3ce6c732ab | |||
| 191fc8f8d4 | |||
| 31c2d45a7a | |||
| dee6355cc5 | |||
| c9b5ea97da | |||
| eaebe83429 | |||
| 7f0b8c5e70 | |||
| 53d51a467d | |||
| 7eeb623b8f | |||
| 6b724f7da4 | |||
| a4409da700 | |||
| 1eb3181c14 | |||
| f7b401a20e | |||
| 5f92ceeea9 | |||
| f0f1fadee1 | |||
| 32f97dc578 | |||
| 631ba2ef0d | |||
| 62de16804b | |||
| 3d919f1235 | |||
| 8ff9506138 | |||
| dd1703469e | |||
| 5f98a70c21 | |||
| bfd64ce96e | |||
| a032e649f5 | |||
| c96a5d5b2b | |||
| a565cc4b73 | |||
| 8d34b76d51 | |||
| 15f89fc636 | |||
| a431277de1 | |||
| 7208ff515e | |||
| 0a79a5e964 | |||
| 8e766daa11 | |||
| 4ded795740 | |||
| 84cb7a4f20 | |||
| cbf0caa88a | |||
| 88d13f0ac9 | |||
| 3ed6be5b4e | |||
| 0340710e5c | |||
| 49acdaa8fd | |||
| ffbc99fac2 | |||
| 6dae005b65 | |||
| 0adc853741 | |||
| 6254d4a983 | |||
| a7db208b8a | |||
| 429bf2c143 | |||
| 8df91e6a17 | |||
| 8656bbbc79 | |||
| 630b7377bd | |||
| 0626a80186 | |||
| 24788b106b | |||
| c5401b21c2 | |||
| 954b56475e | |||
| 53d7e0730c | |||
| cba85cad8d | |||
| 96b73684eb | |||
| aa7fa7b550 | |||
| d229cb46b1 | |||
| 8682e2def8 | |||
| 65ac1ae84a | |||
| 93fd6fa11b | |||
| 67b0365f62 | |||
| f1eda430cd | |||
| 69929f15fb | |||
| d553c7c8e7 | |||
| 4d0b9f1e94 | |||
| fca4ec2b3e | |||
| b75aa6ac08 | |||
| 894ceacd40 | |||
| fbe940139a | |||
| c341ae0a39 | |||
| f9d97c4356 | |||
| b8a5d392c5 | |||
| fd8240241f | |||
| 3c9e493494 | |||
| 786a0154b1 | |||
| dd6ab79e35 | |||
| 2f118c5327 | |||
| a7d1f52ac8 | |||
| 5317f700d7 | |||
| 01eb2d5c84 | |||
| 0893ddcab7 | |||
| e77a7f4385 | |||
| 39e7942dce | |||
| faf5ffe610 | |||
| 8d2dc48261 | |||
| c7cfa8d245 | |||
| 16933abce9 | |||
| 8163b986c9 | |||
| d5a1c52359 | |||
| 26ea4e41cb | |||
| ec9544b9c3 | |||
| 1d0bc1ee66 | |||
| 9729c44d53 | |||
| a7292af3b1 | |||
| c8cbc528eb | |||
| 8735bfe926 | |||
| 25e8c7bc5f | |||
| 499257c8e1 | |||
| 6856283896 | |||
| 20dad9f194 | |||
| 09483e3be4 | |||
| 8ae5708fa2 | |||
| 1e42f85a9c | |||
| 92d71a6612 | |||
| ab2e85840f | |||
| 8257e3f384 | |||
| e40908d67c | |||
| 1abbb43ebd | |||
| 69ecedefad | |||
| 0f6c9d2f75 | |||
| c58fb00f03 | |||
| e67729b2f4 | |||
| 6b1f9a32dd | |||
| 7c8c5e7763 | |||
| 8cb48615af | |||
| 952d1c796e | |||
| 89c22e6d8a | |||
| 16adc30210 | |||
| 2a9c822454 | |||
| 6cae7c0307 | |||
| 6901e5ea5e | |||
| 8a16a7d943 | |||
| 21dd8162b3 | |||
| 67f3fcc5cf | |||
| a1480582d9 | |||
| 713c7a5fd7 | |||
| fb3b3db04e | |||
| b86a1ece01 | |||
| ca0ea6c2f3 | |||
| 1cd59cf2a9 | |||
| 72cf7fd9c2 | |||
| c72ab42c19 | |||
| d73f8d5253 | |||
| 98bedf1bd6 | |||
| 4a28be9a94 | |||
| 393bd88091 | |||
| e5d1ed9439 | |||
| ddfda89fc9 | |||
| 2274806bee | |||
| 7995829790 | |||
| d5031d90c4 | |||
| 2874ad3445 | |||
| 8fc07ee6cd | |||
| 4850a65ed0 | |||
| 4643dcde9c | |||
| 39e03eebcf | |||
| 34193de158 | |||
| 298b9d1f12 | |||
| 6d9254ce25 | |||
| 1a59ba735f | |||
| b2abe552a0 | |||
| 2a972b7fe3 | |||
| f2176e54ba | |||
| 79653a672d | |||
| a6a5e4fda2 | |||
| f6df5bc390 | |||
| b51561cd9b | |||
| 4710b38fad | |||
| 069a4b1706 | |||
| cfd7ca344e | |||
| abc00c76bc | |||
| f8340b94bc | |||
| cc202b886b | |||
| 588a0cc947 | |||
| 8c943c966a | |||
| 1d28fa712f | |||
| 31e019e88a | |||
| 157036c1d2 | |||
| 6354399d55 | |||
| 953223b81b | |||
| 0d261be6ce | |||
| d608153dbb | |||
| f5429c89b6 | |||
| 7331f4910e | |||
| 6b81773075 | |||
| 725b336683 | |||
| f0bcdc6bd8 | |||
| 59e95f0d86 | |||
| 1859c84e6d | |||
| 2b7e1a2cc9 | |||
| 6717215438 | |||
| 5159cc525b | |||
| 9b3403943c | |||
| b4be508741 | |||
| 08d60fb04b | |||
| 7154603567 | |||
| ee9996374c | |||
| 69daa383dd | |||
| 2ca1f7542f | |||
| d4fe6f385a | |||
| 3b0a35f571 | |||
| 448edecdd1 | |||
| d0357cd42f | |||
| 7afdca1121 | |||
| 55b51cb3fa | |||
| 74022a3978 | |||
| d232020de4 | |||
| 27ce394571 | |||
| 413b8d34e4 | |||
| 8f8d936539 | |||
| 197d9639f9 | |||
| 14bd630c1d | |||
| 533799656e | |||
| 1d8554359c | |||
| cc42f2d8be | |||
| ba696888c1 | |||
| a4083bab1a | |||
| 1c6a2f87eb | |||
| 4a5411a957 | |||
| 612a017bc6 | |||
| b8e4db9161 | |||
| 26863284b6 | |||
| 09a771a026 | |||
| f76d545a08 | |||
| 2333c0ca3b | |||
| 4e568f8b99 | |||
| c4913a87e4 | |||
| 4acb121689 | |||
| 3318c55c65 | |||
| d3fb69783d | |||
| 43a94995c2 | |||
| dfa37511ad | |||
| 4745282a95 | |||
| 1d3c6da6c8 | |||
| 623b023ac0 | |||
| bf3b77e1f2 | |||
| d5ca97b1f6 | |||
| fd48fc5f83 | |||
| c89cd6a68c |
+23
-1
@@ -14,6 +14,9 @@ omit =
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
homeassistant/components/dweet.py
|
||||
homeassistant/components/*/dweet.py
|
||||
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
@@ -32,6 +35,12 @@ omit =
|
||||
homeassistant/components/nest.py
|
||||
homeassistant/components/*/nest.py
|
||||
|
||||
homeassistant/components/octoprint.py
|
||||
homeassistant/components/*/octoprint.py
|
||||
|
||||
homeassistant/components/qwikswitch.py
|
||||
homeassistant/components/*/qwikswitch.py
|
||||
|
||||
homeassistant/components/rpi_gpio.py
|
||||
homeassistant/components/*/rpi_gpio.py
|
||||
|
||||
@@ -105,17 +114,24 @@ omit =
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pioneer.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/roku.py
|
||||
homeassistant/components/media_player/samsungtv.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/googlevoice.py
|
||||
@@ -131,6 +147,7 @@ omit =
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
@@ -139,12 +156,14 @@ omit =
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dweet.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/netatmo.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
@@ -155,6 +174,7 @@ omit =
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
@@ -164,6 +184,7 @@ omit =
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
@@ -172,6 +193,7 @@ omit =
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/thermostat/eq3btsmart.py
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Feature requests should go in the forum: https://community.home-assistant.io/c/feature-requests
|
||||
Make sure you run the latest version before reporting an issue. Feature requests should go in the forum: https://community.home-assistant.io/c/feature-requests
|
||||
|
||||
**Home Assistant release (`hass --version`):**
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
**Related issue (if applicable):** #
|
||||
|
||||
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
|
||||
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
```yaml
|
||||
|
||||
@@ -10,6 +12,9 @@
|
||||
|
||||
**Checklist:**
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||
|
||||
If code communicates with devices:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
|
||||
@@ -78,7 +78,10 @@ nosetests.xml
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv
|
||||
.venv
|
||||
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
ctags.tmp
|
||||
|
||||
@@ -21,6 +21,14 @@ RUN script/build_python_openzwave && \
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt
|
||||
|
||||
RUN wget http://www.openssl.org/source/openssl-1.0.2h.tar.gz && \
|
||||
tar -xvzf openssl-1.0.2h.tar.gz && \
|
||||
cd openssl-1.0.2h && \
|
||||
./config --prefix=/usr/ && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf openssl-1.0.2h*
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
|
||||
+7
-5
@@ -1,5 +1,5 @@
|
||||
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant|
|
||||
===========================================================================================================
|
||||
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 is a home automation platform running on Python 3. The
|
||||
goal of Home Assistant is to be able to track and control all devices at
|
||||
@@ -80,9 +80,9 @@ Built home automation on top of your devices:
|
||||
|
||||
The system is built modular so support for other devices or actions can
|
||||
be implemented easily. See also the `section on
|
||||
architecture <https://home-assistant.io/developers/architecture.html>`__
|
||||
architecture <https://home-assistant.io/developers/architecture/>`__
|
||||
and the `section on creating your own
|
||||
components <https://home-assistant.io/developers/creating_components.html>`__.
|
||||
components <https://home-assistant.io/developers/creating_components/>`__.
|
||||
|
||||
If you run into issues while using Home Assistant or during development
|
||||
of a component, check the `Home Assistant help
|
||||
@@ -92,7 +92,9 @@ section <https://home-assistant.io/help/>`__ how to reach us.
|
||||
: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
|
||||
.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
.. |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
|
||||
:target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
:target: https://home-assistant.io/demo/
|
||||
|
||||
+125
-54
@@ -3,11 +3,12 @@ from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from multiprocessing import Process
|
||||
|
||||
from homeassistant.const import (
|
||||
__version__,
|
||||
@@ -87,8 +88,7 @@ def get_arguments():
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in debug mode. Runs in single process to '
|
||||
'enable use of interactive debuggers.')
|
||||
help='Start Home Assistant in debug mode')
|
||||
parser.add_argument(
|
||||
'--open-ui',
|
||||
action='store_true',
|
||||
@@ -123,15 +123,20 @@ def get_arguments():
|
||||
'--restart-osx',
|
||||
action='store_true',
|
||||
help='Restarts on OS X.')
|
||||
if os.name != "nt":
|
||||
parser.add_argument(
|
||||
'--runner',
|
||||
action='store_true',
|
||||
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
|
||||
if os.name == "posix":
|
||||
parser.add_argument(
|
||||
'--daemon',
|
||||
action='store_true',
|
||||
help='Run Home Assistant as daemon')
|
||||
|
||||
arguments = parser.parse_args()
|
||||
if os.name == "nt":
|
||||
if os.name != "posix" or arguments.debug or arguments.runner:
|
||||
arguments.daemon = False
|
||||
|
||||
return arguments
|
||||
|
||||
|
||||
@@ -144,13 +149,21 @@ def daemonize():
|
||||
|
||||
# Decouple fork
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
# Create second fork
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
|
||||
# redirect standard file descriptors to devnull
|
||||
infd = open(os.devnull, 'r')
|
||||
outfd = open(os.devnull, 'a+')
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os.dup2(infd.fileno(), sys.stdin.fileno())
|
||||
os.dup2(outfd.fileno(), sys.stdout.fileno())
|
||||
os.dup2(outfd.fileno(), sys.stderr.fileno())
|
||||
|
||||
|
||||
def check_pid(pid_file):
|
||||
"""Check that HA is not already running."""
|
||||
@@ -161,6 +174,10 @@ def check_pid(pid_file):
|
||||
# PID File does not exist
|
||||
return
|
||||
|
||||
# If we just restarted, we just found our own pidfile.
|
||||
if pid == os.getpid():
|
||||
return
|
||||
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
@@ -220,29 +237,61 @@ def uninstall_osx():
|
||||
print("Home Assistant has been uninstalled.")
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir, args, top_process=False):
|
||||
"""Setup HASS and run.
|
||||
def closefds_osx(min_fd, max_fd):
|
||||
"""Make sure file descriptors get closed when we restart.
|
||||
|
||||
Block until stopped. Will assume it is running in a subprocess unless
|
||||
top_process is set to true.
|
||||
We cannot call close on guarded fds, and we cannot easily test which fds
|
||||
are guarded. But we can set the close-on-exec flag on everything we want to
|
||||
get rid of.
|
||||
"""
|
||||
from fcntl import fcntl, F_GETFD, F_SETFD, FD_CLOEXEC
|
||||
|
||||
for _fd in range(min_fd, max_fd):
|
||||
try:
|
||||
val = fcntl(_fd, F_GETFD)
|
||||
if not val & FD_CLOEXEC:
|
||||
fcntl(_fd, F_SETFD, val | FD_CLOEXEC)
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
|
||||
def cmdline():
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if sys.argv[0].endswith('/__main__.py'):
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir, args):
|
||||
"""Setup HASS and run."""
|
||||
from homeassistant import bootstrap
|
||||
|
||||
# Run a simple daemon runner process on Windows to handle restarts
|
||||
if os.name == 'nt' and '--runner' not in sys.argv:
|
||||
args = cmdline() + ['--runner']
|
||||
while True:
|
||||
try:
|
||||
subprocess.check_call(args)
|
||||
sys.exit(0)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
if exc.returncode != RESTART_EXIT_CODE:
|
||||
sys.exit(exc.returncode)
|
||||
|
||||
if args.demo_mode:
|
||||
config = {
|
||||
'frontend': {},
|
||||
'demo': {}
|
||||
}
|
||||
hass = bootstrap.from_config_dict(
|
||||
config, config_dir=config_dir, daemon=args.daemon,
|
||||
verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
log_rotate_days=args.log_rotate_days)
|
||||
config, config_dir=config_dir, verbose=args.verbose,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
|
||||
else:
|
||||
config_file = ensure_config_file(config_dir)
|
||||
print('Config directory:', config_dir)
|
||||
hass = bootstrap.from_config_file(
|
||||
config_file, daemon=args.daemon, verbose=args.verbose,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
|
||||
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
log_rotate_days=args.log_rotate_days)
|
||||
|
||||
if hass is None:
|
||||
return
|
||||
@@ -256,42 +305,68 @@ def setup_and_run_hass(config_dir, args, top_process=False):
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||
|
||||
print('Starting Home-Assistant')
|
||||
hass.start()
|
||||
exit_code = int(hass.block_till_stopped())
|
||||
|
||||
if not top_process:
|
||||
sys.exit(exit_code)
|
||||
return exit_code
|
||||
|
||||
|
||||
def run_hass_process(hass_proc):
|
||||
"""Run a child hass process. Returns True if it should be restarted."""
|
||||
requested_stop = threading.Event()
|
||||
hass_proc.daemon = True
|
||||
def try_to_restart():
|
||||
"""Attempt to clean up state and start a new homeassistant instance."""
|
||||
# Things should be mostly shut down already at this point, now just try
|
||||
# to clean up things that may have been left behind.
|
||||
sys.stderr.write('Home Assistant attempting to restart.\n')
|
||||
|
||||
def request_stop(*args):
|
||||
"""Request hass stop, *args is for signal handler callback."""
|
||||
requested_stop.set()
|
||||
hass_proc.terminate()
|
||||
# Count remaining threads, ideally there should only be one non-daemonized
|
||||
# thread left (which is us). Nothing we really do with it, but it might be
|
||||
# useful when debugging shutdown/restart issues.
|
||||
nthreads = sum(thread.isAlive() and not thread.isDaemon()
|
||||
for thread in threading.enumerate())
|
||||
if nthreads > 1:
|
||||
sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads))
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
except ValueError:
|
||||
print('Could not bind to SIGTERM. Are you running in a thread?')
|
||||
# Send terminate signal to all processes in our process group which
|
||||
# should be any children that have not themselves changed the process
|
||||
# group id. Don't bother if couldn't even call setpgid.
|
||||
if hasattr(os, 'setpgid'):
|
||||
sys.stderr.write("Signalling child processes to terminate...\n")
|
||||
os.kill(0, signal.SIGTERM)
|
||||
|
||||
hass_proc.start()
|
||||
try:
|
||||
hass_proc.join()
|
||||
except KeyboardInterrupt:
|
||||
request_stop()
|
||||
# wait for child processes to terminate
|
||||
try:
|
||||
hass_proc.join()
|
||||
except KeyboardInterrupt:
|
||||
return False
|
||||
while True:
|
||||
time.sleep(1)
|
||||
if os.waitpid(0, os.WNOHANG) == (0, 0):
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return (not requested_stop.isSet() and
|
||||
hass_proc.exitcode == RESTART_EXIT_CODE,
|
||||
hass_proc.exitcode)
|
||||
elif os.name == 'nt':
|
||||
# Maybe one of the following will work, but how do we indicate which
|
||||
# processes are our children if there is no process group?
|
||||
# os.kill(0, signal.CTRL_C_EVENT)
|
||||
# os.kill(0, signal.CTRL_BREAK_EVENT)
|
||||
pass
|
||||
|
||||
# Try to not leave behind open filedescriptors with the emphasis on try.
|
||||
try:
|
||||
max_fd = os.sysconf("SC_OPEN_MAX")
|
||||
except ValueError:
|
||||
max_fd = 256
|
||||
|
||||
if platform.system() == 'Darwin':
|
||||
closefds_osx(3, max_fd)
|
||||
else:
|
||||
os.closerange(3, max_fd)
|
||||
|
||||
# Now launch into a new instance of Home-Assistant. If this fails we
|
||||
# fall through and exit with error 100 (RESTART_EXIT_CODE) in which case
|
||||
# systemd will restart us when RestartForceExitStatus=100 is set in the
|
||||
# systemd.service file.
|
||||
sys.stderr.write("Restarting Home-Assistant\n")
|
||||
args = cmdline()
|
||||
os.execv(args[0], args)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -325,21 +400,17 @@ def main():
|
||||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
# Run hass in debug mode if requested
|
||||
if args.debug:
|
||||
sys.stderr.write('Running in debug mode. '
|
||||
'Home Assistant will not be able to restart.\n')
|
||||
exit_code = setup_and_run_hass(config_dir, args, top_process=True)
|
||||
if exit_code == RESTART_EXIT_CODE:
|
||||
sys.stderr.write('Home Assistant requested a '
|
||||
'restart in debug mode.\n')
|
||||
return exit_code
|
||||
# Create new process group if we can
|
||||
if hasattr(os, 'setpgid'):
|
||||
try:
|
||||
os.setpgid(0, 0)
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
exit_code = setup_and_run_hass(config_dir, args)
|
||||
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||
try_to_restart()
|
||||
|
||||
# Run hass as child process. Restart if necessary.
|
||||
keep_running = True
|
||||
while keep_running:
|
||||
hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args))
|
||||
keep_running, exit_code = run_hass_process(hass_proc)
|
||||
return exit_code
|
||||
|
||||
|
||||
|
||||
+35
-35
@@ -14,6 +14,7 @@ import homeassistant.components as core_components
|
||||
import homeassistant.components.group as group
|
||||
import homeassistant.config as config_util
|
||||
import homeassistant.core as core
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.util.location as loc_util
|
||||
@@ -103,7 +104,7 @@ def _setup_component(hass, domain, config):
|
||||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
_LOGGER.error('Invalid config for [%s]: %s', domain, ex)
|
||||
cv.log_exception(_LOGGER, ex, domain, config)
|
||||
return False
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
@@ -113,12 +114,11 @@ def _setup_component(hass, domain, config):
|
||||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
_LOGGER.error('Invalid platform config for [%s]: %s. %s',
|
||||
domain, ex, p_config)
|
||||
cv.log_exception(_LOGGER, ex, domain, p_config)
|
||||
return False
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
# Sof if p_name is None we are not going to validate platform
|
||||
# So if p_name is None we are not going to validate platform
|
||||
# (the automation component is one of them)
|
||||
if p_name is None:
|
||||
platforms.append(p_validated)
|
||||
@@ -135,9 +135,8 @@ def _setup_component(hass, domain, config):
|
||||
try:
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.MultipleInvalid as ex:
|
||||
_LOGGER.error(
|
||||
'Invalid platform config for [%s.%s]: %s. %s',
|
||||
domain, p_name, ex, p_config)
|
||||
cv.log_exception(_LOGGER, ex, '{}.{}'
|
||||
.format(domain, p_name), p_validated)
|
||||
return False
|
||||
|
||||
platforms.append(p_validated)
|
||||
@@ -216,7 +215,7 @@ def mount_local_lib_path(config_dir):
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
|
||||
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
verbose=False, daemon=False, skip_pip=False,
|
||||
verbose=False, skip_pip=False,
|
||||
log_rotate_days=None):
|
||||
"""Try to configure Home Assistant from a config dict.
|
||||
|
||||
@@ -229,17 +228,19 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA(
|
||||
config.get(core.DOMAIN, {})))
|
||||
core_config))
|
||||
except vol.MultipleInvalid as ex:
|
||||
_LOGGER.error('Invalid config for [homeassistant]: %s', ex)
|
||||
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
|
||||
return None
|
||||
|
||||
process_ha_config_upgrade(hass)
|
||||
|
||||
if enable_log:
|
||||
enable_logging(hass, verbose, daemon, log_rotate_days)
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
@@ -277,8 +278,8 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
return hass
|
||||
|
||||
|
||||
def from_config_file(config_path, hass=None, verbose=False, daemon=False,
|
||||
skip_pip=True, log_rotate_days=None):
|
||||
def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
|
||||
log_rotate_days=None):
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
@@ -292,7 +293,7 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False,
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
enable_logging(hass, verbose, daemon, log_rotate_days)
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
try:
|
||||
config_dict = config_util.load_yaml_config_file(config_path)
|
||||
@@ -303,28 +304,27 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False,
|
||||
skip_pip=skip_pip)
|
||||
|
||||
|
||||
def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
|
||||
def enable_logging(hass, verbose=False, log_rotate_days=None):
|
||||
"""Setup the logging."""
|
||||
if not daemon:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
"[%(name)s] %(message)s%(reset)s")
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
fmt,
|
||||
datefmt='%y-%m-%d %H:%M:%S',
|
||||
reset=True,
|
||||
log_colors={
|
||||
'DEBUG': 'cyan',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'red',
|
||||
}
|
||||
))
|
||||
except ImportError:
|
||||
pass
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
"[%(name)s] %(message)s%(reset)s")
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
fmt,
|
||||
datefmt='%y-%m-%d %H:%M:%S',
|
||||
reset=True,
|
||||
log_colors={
|
||||
'DEBUG': 'cyan',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'red',
|
||||
}
|
||||
))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
|
||||
@@ -48,6 +48,11 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return hub.available
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The code format as regex."""
|
||||
|
||||
@@ -8,8 +8,7 @@ import enum
|
||||
import logging
|
||||
|
||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers import template, script
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -27,7 +26,14 @@ CONF_ACTION = 'action'
|
||||
|
||||
def setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
_CONFIG.update(config[DOMAIN].get(CONF_INTENTS, {}))
|
||||
intents = config[DOMAIN].get(CONF_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))
|
||||
|
||||
_CONFIG.update(intents)
|
||||
|
||||
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
|
||||
|
||||
@@ -91,7 +97,7 @@ def _handle_alexa(handler, path_match, data):
|
||||
card['content'])
|
||||
|
||||
if action is not None:
|
||||
call_from_config(handler.server.hass, action, True)
|
||||
action.run(response.variables)
|
||||
|
||||
handler.write_json(response.as_dict())
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@ from homeassistant.const import (
|
||||
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
|
||||
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_ERROR_LOG, URL_API_EVENT_FORWARD, URL_API_EVENTS,
|
||||
URL_API_LOG_OUT, URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY,
|
||||
URL_API_STREAM, URL_API_TEMPLATE)
|
||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
|
||||
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_LOG_OUT, URL_API_SERVICES,
|
||||
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import TrackStates
|
||||
from homeassistant.helpers import template
|
||||
@@ -37,13 +38,18 @@ def setup(hass, config):
|
||||
# /api - for validation purposes
|
||||
hass.http.register_path('GET', URL_API, _handle_get_api)
|
||||
|
||||
# /api/stream
|
||||
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
|
||||
|
||||
# /api/config
|
||||
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
|
||||
|
||||
# /states
|
||||
# /api/discovery_info
|
||||
hass.http.register_path('GET', URL_API_DISCOVERY_INFO,
|
||||
_handle_get_api_discovery_info,
|
||||
require_auth=False)
|
||||
|
||||
# /api/stream
|
||||
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
|
||||
|
||||
# /api/states
|
||||
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
@@ -58,13 +64,13 @@ def setup(hass, config):
|
||||
'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_delete_state_entity)
|
||||
|
||||
# /events
|
||||
# /api/events
|
||||
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
|
||||
hass.http.register_path(
|
||||
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
||||
_handle_api_post_events_event)
|
||||
|
||||
# /services
|
||||
# /api/services
|
||||
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
|
||||
hass.http.register_path(
|
||||
'POST',
|
||||
@@ -73,23 +79,23 @@ def setup(hass, config):
|
||||
r'(?P<service>[a-zA-Z\._0-9]+)')),
|
||||
_handle_post_api_services_domain_service)
|
||||
|
||||
# /event_forwarding
|
||||
# /api/event_forwarding
|
||||
hass.http.register_path(
|
||||
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
|
||||
hass.http.register_path(
|
||||
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
|
||||
|
||||
# /components
|
||||
# /api/components
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_COMPONENTS, _handle_get_api_components)
|
||||
|
||||
# /error_log
|
||||
# /api/error_log
|
||||
hass.http.register_path('GET', URL_API_ERROR_LOG,
|
||||
_handle_get_api_error_log)
|
||||
|
||||
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
|
||||
|
||||
# /template
|
||||
# /api/template
|
||||
hass.http.register_path('POST', URL_API_TEMPLATE,
|
||||
_handle_post_api_template)
|
||||
|
||||
@@ -176,6 +182,17 @@ def _handle_get_api_config(handler, path_match, data):
|
||||
handler.write_json(handler.server.hass.config.as_dict())
|
||||
|
||||
|
||||
def _handle_get_api_discovery_info(handler, path_match, data):
|
||||
needs_auth = (handler.server.hass.config.api.api_password is not None)
|
||||
params = {
|
||||
'base_url': handler.server.hass.config.api.base_url,
|
||||
'location_name': handler.server.hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
}
|
||||
handler.write_json(params)
|
||||
|
||||
|
||||
def _handle_get_api_states(handler, path_match, data):
|
||||
"""Return a dict containing all entity ids and their state."""
|
||||
handler.write_json(handler.server.hass.states.all())
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
DOMAIN = "arduino"
|
||||
REQUIREMENTS = ['PyMata==2.07a']
|
||||
REQUIREMENTS = ['PyMata==2.12']
|
||||
BOARD = None
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,7 +69,7 @@ class ArduinoBoard(object):
|
||||
self._board.ANALOG)
|
||||
elif mode == 'digital' and direction == 'in':
|
||||
self._board.set_pin_mode(pin,
|
||||
self._board.OUTPUT,
|
||||
self._board.INPUT,
|
||||
self._board.DIGITAL)
|
||||
elif mode == 'digital' and direction == 'out':
|
||||
self._board.set_pin_mode(pin,
|
||||
|
||||
@@ -9,10 +9,10 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.helpers import extract_domain_configs
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
from homeassistant.loader import get_platform
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -74,10 +74,11 @@ _CONDITION_SCHEMA = vol.Any(
|
||||
[
|
||||
vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN),
|
||||
CONF_PLATFORM: str,
|
||||
CONF_CONDITION: str,
|
||||
}, extra=vol.ALLOW_EXTRA),
|
||||
_platform_validator(METHOD_IF_ACTION, 'IF_ACTION_SCHEMA'),
|
||||
)
|
||||
cv.has_at_least_one_key(CONF_PLATFORM, CONF_CONDITION),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -88,21 +89,23 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
|
||||
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
|
||||
CONF_CONDITION: _CONDITION_SCHEMA,
|
||||
vol.Required(CONF_ACTION): cv.SERVICE_SCHEMA,
|
||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the automation."""
|
||||
success = False
|
||||
for config_key in extract_domain_configs(config, DOMAIN):
|
||||
conf = config[config_key]
|
||||
|
||||
for list_no, config_block in enumerate(conf):
|
||||
name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key,
|
||||
list_no))
|
||||
_setup_automation(hass, config_block, name, config)
|
||||
success = (_setup_automation(hass, config_block, name, config) or
|
||||
success)
|
||||
|
||||
return True
|
||||
return success
|
||||
|
||||
|
||||
def _setup_automation(hass, config_block, name, config):
|
||||
@@ -122,12 +125,13 @@ def _setup_automation(hass, config_block, name, config):
|
||||
|
||||
def _get_action(hass, config, name):
|
||||
"""Return an action based on a configuration."""
|
||||
def action():
|
||||
script_obj = script.Script(hass, config, name)
|
||||
|
||||
def action(variables=None):
|
||||
"""Action to be executed."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
|
||||
call_from_config(hass, config)
|
||||
script_obj.run(variables)
|
||||
|
||||
return action
|
||||
|
||||
@@ -137,6 +141,11 @@ def _process_if(hass, config, p_config, action):
|
||||
cond_type = p_config.get(CONF_CONDITION_TYPE,
|
||||
DEFAULT_CONDITION_TYPE).lower()
|
||||
|
||||
# Deprecated since 0.19 - 5/5/2016
|
||||
if cond_type != DEFAULT_CONDITION_TYPE:
|
||||
_LOGGER.warning('Using condition_type: "or" is deprecated. Please use '
|
||||
'"condition: or" instead.')
|
||||
|
||||
if_configs = p_config.get(CONF_CONDITION)
|
||||
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
|
||||
|
||||
@@ -145,38 +154,47 @@ def _process_if(hass, config, p_config, action):
|
||||
|
||||
checks = []
|
||||
for if_config in if_configs:
|
||||
platform = _resolve_platform(METHOD_IF_ACTION, hass, config,
|
||||
if_config.get(CONF_PLATFORM))
|
||||
if platform is None:
|
||||
continue
|
||||
# Deprecated except for used by use_trigger_values
|
||||
# since 0.19 - 5/5/2016
|
||||
if CONF_PLATFORM in if_config:
|
||||
if not use_trigger:
|
||||
_LOGGER.warning("Please switch your condition configuration "
|
||||
"to use 'condition' instead of 'platform'.")
|
||||
if_config = dict(if_config)
|
||||
if_config[CONF_CONDITION] = if_config.pop(CONF_PLATFORM)
|
||||
|
||||
check = platform.if_action(hass, if_config)
|
||||
# To support use_trigger_values with state trigger accepting
|
||||
# multiple entity_ids to monitor.
|
||||
if_entity_id = if_config.get(ATTR_ENTITY_ID)
|
||||
if isinstance(if_entity_id, list) and len(if_entity_id) == 1:
|
||||
if_config[ATTR_ENTITY_ID] = if_entity_id[0]
|
||||
|
||||
# Invalid conditions are allowed if we base it on trigger
|
||||
if check is None and not use_trigger:
|
||||
return None
|
||||
|
||||
checks.append(check)
|
||||
try:
|
||||
checks.append(condition.from_config(if_config))
|
||||
except HomeAssistantError as ex:
|
||||
# Invalid conditions are allowed if we base it on trigger
|
||||
if use_trigger:
|
||||
_LOGGER.warning('Ignoring invalid condition: %s', ex)
|
||||
else:
|
||||
_LOGGER.warning('Invalid condition: %s', ex)
|
||||
return None
|
||||
|
||||
if cond_type == CONDITION_TYPE_AND:
|
||||
def if_action():
|
||||
def if_action(variables=None):
|
||||
"""AND all conditions."""
|
||||
if all(check() for check in checks):
|
||||
action()
|
||||
if all(check(hass, variables) for check in checks):
|
||||
action(variables)
|
||||
else:
|
||||
def if_action():
|
||||
def if_action(variables=None):
|
||||
"""OR all conditions."""
|
||||
if any(check() for check in checks):
|
||||
action()
|
||||
if any(check(hass, variables) for check in checks):
|
||||
action(variables)
|
||||
|
||||
return if_action
|
||||
|
||||
|
||||
def _process_trigger(hass, config, trigger_configs, name, action):
|
||||
"""Setup the triggers."""
|
||||
if isinstance(trigger_configs, dict):
|
||||
trigger_configs = [trigger_configs]
|
||||
|
||||
for conf in trigger_configs:
|
||||
platform = _resolve_platform(METHOD_TRIGGER, hass, config,
|
||||
conf.get(CONF_PLATFORM))
|
||||
|
||||
@@ -6,27 +6,38 @@ at https://home-assistant.io/components/automation/#event-trigger
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
CONF_EVENT_TYPE = "event_type"
|
||||
CONF_EVENT_DATA = "event_data"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'event',
|
||||
vol.Required(CONF_EVENT_TYPE): cv.string,
|
||||
vol.Optional(CONF_EVENT_DATA): dict,
|
||||
})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
|
||||
if event_type is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE)
|
||||
return False
|
||||
|
||||
event_data = config.get(CONF_EVENT_DATA)
|
||||
|
||||
def handle_event(event):
|
||||
"""Listen for events and calls the action when data matches."""
|
||||
if not event_data or all(val == event.data.get(key) for key, val
|
||||
in event_data.items()):
|
||||
action()
|
||||
action({
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
|
||||
hass.bus.listen(event_type, handle_event)
|
||||
return True
|
||||
|
||||
@@ -30,7 +30,14 @@ def trigger(hass, config, action):
|
||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||
"""Listen for MQTT messages."""
|
||||
if payload is None or payload == msg_payload:
|
||||
action()
|
||||
action({
|
||||
'trigger': {
|
||||
'platform': 'mqtt',
|
||||
'topic': msg_topic,
|
||||
'payload': msg_payload,
|
||||
'qos': qos,
|
||||
}
|
||||
})
|
||||
|
||||
mqtt.subscribe(hass, topic, mqtt_automation_listener)
|
||||
|
||||
|
||||
@@ -5,101 +5,65 @@ For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_BELOW = "below"
|
||||
CONF_ABOVE = "above"
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'numeric_state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
CONF_BELOW: vol.Coerce(float),
|
||||
CONF_ABOVE: vol.Coerce(float),
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _renderer(hass, value_template, state):
|
||||
"""Render the state value."""
|
||||
if value_template is None:
|
||||
return state.state
|
||||
|
||||
return template.render(hass, value_template, {'state': state})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
|
||||
return False
|
||||
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
if below is None and above is None:
|
||||
_LOGGER.error("Missing configuration key."
|
||||
" One of %s or %s is required",
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return False
|
||||
|
||||
renderer = partial(_renderer, hass, value_template)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
# Fire action if we go from outside range into range
|
||||
if _in_range(above, below, renderer(to_s)) and \
|
||||
(from_s is None or not _in_range(above, below, renderer(from_s))):
|
||||
action()
|
||||
if to_s is None:
|
||||
return
|
||||
|
||||
variables = {
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity,
|
||||
'below': below,
|
||||
'above': above,
|
||||
}
|
||||
}
|
||||
|
||||
# If new one doesn't match, nothing to do
|
||||
if not condition.numeric_state(
|
||||
hass, to_s, below, above, value_template, variables):
|
||||
return
|
||||
|
||||
# Only match if old didn't exist or existed but didn't match
|
||||
# Written as: skip if old one did exist and matched
|
||||
if from_s is not None and condition.numeric_state(
|
||||
hass, from_s, below, above, value_template, variables):
|
||||
return
|
||||
|
||||
variables['trigger']['from_state'] = from_s
|
||||
variables['trigger']['to_state'] = to_s
|
||||
|
||||
action(variables)
|
||||
|
||||
track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
"""Wrap action method with state based condition."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
|
||||
return None
|
||||
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
if below is None and above is None:
|
||||
_LOGGER.error("Missing configuration key."
|
||||
" One of %s or %s is required",
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
return None
|
||||
|
||||
renderer = partial(_renderer, hass, value_template)
|
||||
|
||||
def if_numeric_state():
|
||||
"""Test numeric state condition."""
|
||||
state = hass.states.get(entity_id)
|
||||
return state is not None and _in_range(above, below, renderer(state))
|
||||
|
||||
return if_numeric_state
|
||||
|
||||
|
||||
def _in_range(range_start, range_end, value):
|
||||
"""Check if value is inside the range."""
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
_LOGGER.warning("Value returned from template is not a number: %s",
|
||||
value)
|
||||
return False
|
||||
|
||||
if range_start is not None and range_end is not None:
|
||||
return float(range_start) <= value < float(range_end)
|
||||
elif range_end is not None:
|
||||
return value < float(range_end)
|
||||
else:
|
||||
return float(range_start) <= value
|
||||
|
||||
@@ -4,15 +4,11 @@ Offer state listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#state-trigger
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, CONF_PLATFORM)
|
||||
from homeassistant.components.automation.time import (
|
||||
CONF_HOURS, CONF_MINUTES, CONF_SECONDS)
|
||||
from homeassistant.helpers.event import track_state_change, track_point_in_time
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -22,46 +18,19 @@ CONF_TO = "to"
|
||||
CONF_STATE = "state"
|
||||
CONF_FOR = "for"
|
||||
|
||||
BASE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
# These are str on purpose. Want to catch YAML conversions
|
||||
CONF_STATE: str,
|
||||
CONF_FOR: vol.All(vol.Schema({
|
||||
CONF_HOURS: vol.Coerce(int),
|
||||
CONF_MINUTES: vol.Coerce(int),
|
||||
CONF_SECONDS: vol.Coerce(int),
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS)),
|
||||
})
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema(vol.All(
|
||||
BASE_SCHEMA.extend({
|
||||
TRIGGER_SCHEMA = vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
# 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))
|
||||
))
|
||||
|
||||
IF_ACTION_SCHEMA = vol.Schema(vol.All(
|
||||
BASE_SCHEMA,
|
||||
cv.key_dependency(CONF_FOR, CONF_STATE)
|
||||
))
|
||||
|
||||
|
||||
def get_time_config(config):
|
||||
"""Helper function to extract the time specified in the configuration."""
|
||||
if CONF_FOR not in config:
|
||||
return None
|
||||
|
||||
hours = config[CONF_FOR].get(CONF_HOURS)
|
||||
minutes = config[CONF_FOR].get(CONF_MINUTES)
|
||||
seconds = config[CONF_FOR].get(CONF_SECONDS)
|
||||
|
||||
return timedelta(hours=(hours or 0.0),
|
||||
minutes=(minutes or 0.0),
|
||||
seconds=(seconds or 0.0))
|
||||
)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
@@ -69,52 +38,48 @@ def trigger(hass, config, action):
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
||||
time_delta = get_time_config(config)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
action({
|
||||
'trigger': {
|
||||
'platform': 'state',
|
||||
'entity_id': entity,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
'for': time_delta,
|
||||
}
|
||||
})
|
||||
|
||||
if time_delta is None:
|
||||
call_action()
|
||||
return
|
||||
|
||||
def state_for_listener(now):
|
||||
"""Fire on state changes after a delay and calls action."""
|
||||
hass.bus.remove_listener(
|
||||
EVENT_STATE_CHANGED, for_state_listener)
|
||||
action()
|
||||
EVENT_STATE_CHANGED, attached_state_for_cancel)
|
||||
call_action()
|
||||
|
||||
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
||||
"""Fire on changes and cancel for listener if changed."""
|
||||
if inner_to_s == to_s:
|
||||
if inner_to_s.state == to_s.state:
|
||||
return
|
||||
hass.bus.remove_listener(EVENT_TIME_CHANGED, for_time_listener)
|
||||
hass.bus.remove_listener(
|
||||
EVENT_STATE_CHANGED, for_state_listener)
|
||||
hass.bus.remove_listener(EVENT_TIME_CHANGED,
|
||||
attached_state_for_listener)
|
||||
hass.bus.remove_listener(EVENT_STATE_CHANGED,
|
||||
attached_state_for_cancel)
|
||||
|
||||
if time_delta is not None:
|
||||
target_tm = dt_util.utcnow() + time_delta
|
||||
for_time_listener = track_point_in_time(
|
||||
hass, state_for_listener, target_tm)
|
||||
for_state_listener = track_state_change(
|
||||
hass, entity_id, state_for_cancel_listener,
|
||||
MATCH_ALL, MATCH_ALL)
|
||||
else:
|
||||
action()
|
||||
attached_state_for_listener = track_point_in_time(
|
||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||
|
||||
attached_state_for_cancel = track_state_change(
|
||||
hass, entity, state_for_cancel_listener)
|
||||
|
||||
track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
"""Wrap action method with state based condition."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
state = config.get(CONF_STATE)
|
||||
time_delta = get_time_config(config)
|
||||
|
||||
def if_state():
|
||||
"""Test if condition."""
|
||||
is_state = hass.states.is_state(entity_id, state)
|
||||
return (time_delta is None and is_state or
|
||||
time_delta is not None and
|
||||
dt_util.utcnow() - time_delta >
|
||||
hass.states.get(entity_id).last_changed)
|
||||
|
||||
return if_state
|
||||
|
||||
@@ -9,108 +9,41 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import sun
|
||||
from homeassistant.const import (
|
||||
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
|
||||
from homeassistant.helpers.event import track_sunrise, track_sunset
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['sun']
|
||||
|
||||
CONF_OFFSET = 'offset'
|
||||
CONF_EVENT = 'event'
|
||||
CONF_BEFORE = "before"
|
||||
CONF_BEFORE_OFFSET = "before_offset"
|
||||
CONF_AFTER = "after"
|
||||
CONF_AFTER_OFFSET = "after_offset"
|
||||
|
||||
EVENT_SUNSET = 'sunset'
|
||||
EVENT_SUNRISE = 'sunrise'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_SUN_EVENT = vol.All(vol.Lower, vol.Any(EVENT_SUNRISE, EVENT_SUNSET))
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'sun',
|
||||
vol.Required(CONF_EVENT): _SUN_EVENT,
|
||||
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_offset,
|
||||
vol.Required(CONF_EVENT): cv.sun_event,
|
||||
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
|
||||
})
|
||||
|
||||
IF_ACTION_SCHEMA = vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'sun',
|
||||
CONF_BEFORE: _SUN_EVENT,
|
||||
CONF_AFTER: _SUN_EVENT,
|
||||
vol.Required(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_offset,
|
||||
vol.Required(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_offset,
|
||||
}),
|
||||
cv.has_at_least_one_key(CONF_BEFORE, CONF_AFTER),
|
||||
)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
offset = config.get(CONF_OFFSET)
|
||||
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
action({
|
||||
'trigger': {
|
||||
'platform': 'sun',
|
||||
'event': event,
|
||||
'offset': offset,
|
||||
},
|
||||
})
|
||||
|
||||
# Do something to call action
|
||||
if event == EVENT_SUNRISE:
|
||||
track_sunrise(hass, action, offset)
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
track_sunrise(hass, call_action, offset)
|
||||
else:
|
||||
track_sunset(hass, action, offset)
|
||||
track_sunset(hass, call_action, offset)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
"""Wrap action method with sun based condition."""
|
||||
before = config.get(CONF_BEFORE)
|
||||
after = config.get(CONF_AFTER)
|
||||
before_offset = config.get(CONF_BEFORE_OFFSET)
|
||||
after_offset = config.get(CONF_AFTER_OFFSET)
|
||||
|
||||
if before is None:
|
||||
def before_func():
|
||||
"""Return no point in time."""
|
||||
return None
|
||||
elif before == EVENT_SUNRISE:
|
||||
def before_func():
|
||||
"""Return time before sunrise."""
|
||||
return sun.next_rising(hass) + before_offset
|
||||
else:
|
||||
def before_func():
|
||||
"""Return time before sunset."""
|
||||
return sun.next_setting(hass) + before_offset
|
||||
|
||||
if after is None:
|
||||
def after_func():
|
||||
"""Return no point in time."""
|
||||
return None
|
||||
elif after == EVENT_SUNRISE:
|
||||
def after_func():
|
||||
"""Return time after sunrise."""
|
||||
return sun.next_rising(hass) + after_offset
|
||||
else:
|
||||
def after_func():
|
||||
"""Return time after sunset."""
|
||||
return sun.next_setting(hass) + after_offset
|
||||
|
||||
def time_if():
|
||||
"""Validate time based if-condition."""
|
||||
now = dt_util.now()
|
||||
before = before_func()
|
||||
after = after_func()
|
||||
|
||||
if before is not None and now > now.replace(hour=before.hour,
|
||||
minute=before.minute):
|
||||
return False
|
||||
|
||||
if after is not None and now < now.replace(hour=after.hour,
|
||||
minute=after.minute):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return time_if
|
||||
|
||||
@@ -9,9 +9,9 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED, CONF_PLATFORM)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL)
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
@@ -30,40 +30,24 @@ def trigger(hass, config, action):
|
||||
# Local variable to keep track of if the action has already been triggered
|
||||
already_triggered = False
|
||||
|
||||
def event_listener(event):
|
||||
def state_changed_listener(entity_id, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal already_triggered
|
||||
template_result = _check_template(hass, value_template)
|
||||
template_result = condition.template(hass, value_template)
|
||||
|
||||
# Check to see if template returns true
|
||||
if template_result and not already_triggered:
|
||||
already_triggered = True
|
||||
action()
|
||||
action({
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
},
|
||||
})
|
||||
elif not template_result:
|
||||
already_triggered = False
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, event_listener)
|
||||
track_state_change(hass, MATCH_ALL, state_changed_listener)
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
"""Wrap action method with state based condition."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
return lambda: _check_template(hass, value_template)
|
||||
|
||||
|
||||
def _check_template(hass, value_template):
|
||||
"""Check if result of template is true."""
|
||||
try:
|
||||
value = template.render(hass, value_template, {})
|
||||
except TemplateError as ex:
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
# Common during HA startup - so just a warning
|
||||
_LOGGER.warning(ex)
|
||||
else:
|
||||
_LOGGER.error(ex)
|
||||
return False
|
||||
|
||||
return value.lower() == 'true'
|
||||
|
||||
@@ -6,99 +6,48 @@ at https://home-assistant.io/components/automation/#time-trigger
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
|
||||
CONF_HOURS = "hours"
|
||||
CONF_MINUTES = "minutes"
|
||||
CONF_SECONDS = "seconds"
|
||||
CONF_BEFORE = "before"
|
||||
CONF_AFTER = "after"
|
||||
CONF_WEEKDAY = "weekday"
|
||||
|
||||
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'time',
|
||||
CONF_AFTER: cv.time,
|
||||
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES,
|
||||
CONF_SECONDS, CONF_AFTER))
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AFTER in config:
|
||||
after = dt_util.parse_time(config[CONF_AFTER])
|
||||
if after is None:
|
||||
_error_time(config[CONF_AFTER], CONF_AFTER)
|
||||
return False
|
||||
after = config.get(CONF_AFTER)
|
||||
hours, minutes, seconds = after.hour, after.minute, after.second
|
||||
elif (CONF_HOURS in config or CONF_MINUTES in config or
|
||||
CONF_SECONDS in config):
|
||||
else:
|
||||
hours = config.get(CONF_HOURS)
|
||||
minutes = config.get(CONF_MINUTES)
|
||||
seconds = config.get(CONF_SECONDS)
|
||||
else:
|
||||
_LOGGER.error('One of %s, %s, %s OR %s needs to be specified',
|
||||
CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER)
|
||||
return False
|
||||
|
||||
def time_automation_listener(now):
|
||||
"""Listen for time changes and calls action."""
|
||||
action()
|
||||
action({
|
||||
'trigger': {
|
||||
'platform': 'time',
|
||||
'now': now,
|
||||
},
|
||||
})
|
||||
|
||||
track_time_change(hass, time_automation_listener,
|
||||
hour=hours, minute=minutes, second=seconds)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
"""Wrap action method with time based condition."""
|
||||
before = config.get(CONF_BEFORE)
|
||||
after = config.get(CONF_AFTER)
|
||||
weekday = config.get(CONF_WEEKDAY)
|
||||
|
||||
if before is None and after is None and weekday is None:
|
||||
_LOGGER.error(
|
||||
"Missing if-condition configuration key %s, %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY)
|
||||
return None
|
||||
|
||||
if before is not None:
|
||||
before = dt_util.parse_time(before)
|
||||
if before is None:
|
||||
_error_time(before, CONF_BEFORE)
|
||||
return None
|
||||
|
||||
if after is not None:
|
||||
after = dt_util.parse_time(after)
|
||||
if after is None:
|
||||
_error_time(after, CONF_AFTER)
|
||||
return None
|
||||
|
||||
def time_if():
|
||||
"""Validate time based if-condition."""
|
||||
now = dt_util.now()
|
||||
if before is not None and now > now.replace(hour=before.hour,
|
||||
minute=before.minute):
|
||||
return False
|
||||
|
||||
if after is not None and now < now.replace(hour=after.hour,
|
||||
minute=after.minute):
|
||||
return False
|
||||
|
||||
if weekday is not None:
|
||||
now_weekday = WEEKDAYS[now.weekday()]
|
||||
|
||||
if isinstance(weekday, str) and weekday != now_weekday or \
|
||||
now_weekday not in weekday:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return time_if
|
||||
|
||||
|
||||
def _error_time(value, key):
|
||||
"""Helper method to print error."""
|
||||
_LOGGER.error(
|
||||
"Received invalid value for '%s': %s", key, value)
|
||||
if isinstance(value, int):
|
||||
_LOGGER.error('Make sure you wrap time values in quotes')
|
||||
|
||||
@@ -6,33 +6,24 @@ at https://home-assistant.io/components/automation/#zone-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zone
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL, CONF_PLATFORM)
|
||||
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM)
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import (
|
||||
condition, config_validation as cv, location)
|
||||
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_ZONE = "zone"
|
||||
CONF_EVENT = "event"
|
||||
EVENT_ENTER = "enter"
|
||||
EVENT_LEAVE = "leave"
|
||||
DEFAULT_EVENT = EVENT_ENTER
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'zone',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(CONF_ZONE): cv.entity_id,
|
||||
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
|
||||
vol.Any(EVENT_ENTER, EVENT_LEAVE),
|
||||
})
|
||||
|
||||
IF_ACTION_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'zone',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_ZONE): cv.entity_id,
|
||||
})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
@@ -42,46 +33,32 @@ def trigger(hass, config, action):
|
||||
|
||||
def zone_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
if from_s and None in (from_s.attributes.get(ATTR_LATITUDE),
|
||||
from_s.attributes.get(ATTR_LONGITUDE)) or \
|
||||
None in (to_s.attributes.get(ATTR_LATITUDE),
|
||||
to_s.attributes.get(ATTR_LONGITUDE)):
|
||||
if from_s and not location.has_location(from_s) or \
|
||||
not location.has_location(to_s):
|
||||
return
|
||||
|
||||
from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None
|
||||
to_match = _in_zone(hass, zone_entity_id, to_s)
|
||||
zone_state = hass.states.get(zone_entity_id)
|
||||
if from_s:
|
||||
from_match = condition.zone(hass, zone_state, from_s)
|
||||
else:
|
||||
from_match = False
|
||||
to_match = condition.zone(hass, zone_state, to_s)
|
||||
|
||||
# pylint: disable=too-many-boolean-expressions
|
||||
if event == EVENT_ENTER and not from_match and to_match or \
|
||||
event == EVENT_LEAVE and from_match and not to_match:
|
||||
action()
|
||||
action({
|
||||
'trigger': {
|
||||
'platform': 'zone',
|
||||
'entity_id': entity,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
'zone': zone_state,
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
|
||||
track_state_change(
|
||||
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
"""Wrap action method with zone based condition."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
|
||||
def if_in_zone():
|
||||
"""Test if condition."""
|
||||
return _in_zone(hass, zone_entity_id, hass.states.get(entity_id))
|
||||
|
||||
return if_in_zone
|
||||
|
||||
|
||||
def _in_zone(hass, zone_entity_id, state):
|
||||
"""Check if state is in zone."""
|
||||
if not state or None in (state.attributes.get(ATTR_LATITUDE),
|
||||
state.attributes.get(ATTR_LONGITUDE)):
|
||||
return False
|
||||
|
||||
zone_state = hass.states.get(zone_entity_id)
|
||||
return zone_state and zone.in_zone(
|
||||
zone_state, state.attributes.get(ATTR_LATITUDE),
|
||||
state.attributes.get(ATTR_LONGITUDE),
|
||||
state.attributes.get(ATTR_GPS_ACCURACY, 0))
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""
|
||||
Support for custom shell commands to to retrieve values.
|
||||
Support for custom shell commands to retrieve values.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.command/
|
||||
https://home-assistant.io/components/binary_sensor.command_line/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
SENSOR_CLASSES)
|
||||
from homeassistant.components.sensor.command_line import CommandSensorData
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.helpers import template
|
||||
@@ -15,6 +16,7 @@ from homeassistant.helpers import template
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Binary Command Sensor"
|
||||
DEFAULT_SENSOR_CLASS = None
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
@@ -29,28 +31,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error('Missing required variable: "command"')
|
||||
return False
|
||||
|
||||
sensor_class = config.get('sensor_class')
|
||||
if sensor_class not in SENSOR_CLASSES:
|
||||
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
|
||||
sensor_class = DEFAULT_SENSOR_CLASS
|
||||
|
||||
data = CommandSensorData(config.get('command'))
|
||||
|
||||
add_devices([CommandBinarySensor(
|
||||
hass,
|
||||
data,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
sensor_class,
|
||||
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
||||
config.get(CONF_VALUE_TEMPLATE)
|
||||
)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class CommandBinarySensor(BinarySensorDevice):
|
||||
"""Represent a command line binary sensor."""
|
||||
|
||||
def __init__(self, hass, data, name, payload_on,
|
||||
def __init__(self, hass, data, name, sensor_class, payload_on,
|
||||
payload_off, value_template):
|
||||
"""Initialize the Command line binary sensor."""
|
||||
self._hass = hass
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._sensor_class = sensor_class
|
||||
self._state = False
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
@@ -67,6 +76,11 @@ class CommandBinarySensor(BinarySensorDevice):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@ property
|
||||
def sensor_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._sensor_class
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
self.data.update()
|
||||
|
||||
@@ -6,10 +6,10 @@ https://home-assistant.io/components/binary_sensor.mysensors/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.binary_sensor import (SENSOR_CLASSES,
|
||||
BinarySensorDevice)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import STATE_ON
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
@@ -22,8 +22,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
mysensors = get_component('mysensors')
|
||||
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||
# states. Map them in a dict of lists.
|
||||
@@ -48,81 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
map_sv_types, devices, add_devices, MySensorsBinarySensor))
|
||||
|
||||
|
||||
class MySensorsBinarySensor(BinarySensorDevice):
|
||||
"""Represent the value of a MySensors child node."""
|
||||
|
||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||
|
||||
def __init__(
|
||||
self, gateway, node_id, child_id, name, value_type, child_type):
|
||||
"""
|
||||
Setup class attributes on instantiation.
|
||||
|
||||
Args:
|
||||
gateway (GatewayWrapper): Gateway object.
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
child_type (str): Child type of child.
|
||||
|
||||
Attributes:
|
||||
gateway (GatewayWrapper): Gateway object.
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
_name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
child_type (str): Child type of child.
|
||||
battery_level (int): Node battery level.
|
||||
_values (dict): Child values. Non state values set as state attributes.
|
||||
mysensors (module): Mysensors main component module.
|
||||
"""
|
||||
self.gateway = gateway
|
||||
self.node_id = node_id
|
||||
self.child_id = child_id
|
||||
self._name = name
|
||||
self.value_type = value_type
|
||||
self.child_type = child_type
|
||||
self.battery_level = 0
|
||||
self._values = {}
|
||||
self.mysensors = get_component('mysensors')
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Mysensor gateway pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of this entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
address = getattr(self.gateway, 'server_address', None)
|
||||
if address:
|
||||
device = '{}:{}'.format(address[0], address[1])
|
||||
else:
|
||||
device = self.gateway.port
|
||||
attr = {
|
||||
self.mysensors.ATTR_DEVICE: device,
|
||||
self.mysensors.ATTR_NODE_ID: self.node_id,
|
||||
self.mysensors.ATTR_CHILD_ID: self.child_id,
|
||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||
}
|
||||
|
||||
set_req = self.gateway.const.SetReq
|
||||
|
||||
for value_type, value in self._values.items():
|
||||
if value_type != self.value_type:
|
||||
try:
|
||||
attr[set_req(value_type).name] = value
|
||||
except ValueError:
|
||||
_LOGGER.error('value_type %s is not valid for mysensors '
|
||||
'version %s', value_type,
|
||||
self.gateway.version)
|
||||
return attr
|
||||
class MySensorsBinarySensor(
|
||||
mysensors.MySensorsDeviceEntity, BinarySensorDevice):
|
||||
"""Represent the value of a MySensors Binary Sensor child node."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -150,23 +76,3 @@ class MySensorsBinarySensor(BinarySensorDevice):
|
||||
})
|
||||
if class_map.get(self.child_type) in SENSOR_CLASSES:
|
||||
return class_map.get(self.child_type)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.value_type in self._values
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest values from a sensor."""
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
child = node.children[self.child_id]
|
||||
for value_type, value in child.values.items():
|
||||
_LOGGER.debug(
|
||||
"%s: value_type %s, value = %s", self._name, value_type, value)
|
||||
if value_type == self.gateway.const.SetReq.V_TRIPPED:
|
||||
self._values[value_type] = STATE_ON if int(
|
||||
value) == 1 else STATE_OFF
|
||||
else:
|
||||
self._values[value_type] = value
|
||||
|
||||
self.battery_level = node.battery_level
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Support for monitoring OctoPrint binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.octoprint/
|
||||
"""
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ["octoprint"]
|
||||
|
||||
SENSOR_TYPES = {
|
||||
# API Endpoint, Group, Key, unit
|
||||
"Printing": ["printer", "state", "printing", None],
|
||||
"Printing Error": ["printer", "state", "error", None]
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the available OctoPrint binary sensors."""
|
||||
octoprint = get_component('octoprint')
|
||||
name = config.get(CONF_NAME, "OctoPrint")
|
||||
monitored_conditions = config.get("monitored_conditions",
|
||||
SENSOR_TYPES.keys())
|
||||
|
||||
devices = []
|
||||
for octo_type in monitored_conditions:
|
||||
if octo_type in SENSOR_TYPES:
|
||||
new_sensor = OctoPrintBinarySensor(octoprint.OCTOPRINT,
|
||||
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)
|
||||
else:
|
||||
_LOGGER.error("Unknown OctoPrint sensor type: %s", octo_type)
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class OctoPrintBinarySensor(BinarySensorDevice):
|
||||
"""Representation an OctoPrint binary sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, api, condition, sensor_type, sensor_name,
|
||||
unit, endpoint, group, tool=None):
|
||||
"""Initialize a new OctoPrint sensor."""
|
||||
self.sensor_name = sensor_name
|
||||
if tool is None:
|
||||
self._name = sensor_name + ' ' + condition
|
||||
else:
|
||||
self._name = sensor_name + ' ' + condition
|
||||
self.sensor_type = sensor_type
|
||||
self.api = api
|
||||
self._state = False
|
||||
self._unit_of_measurement = unit
|
||||
self.api_endpoint = endpoint
|
||||
self.api_group = group
|
||||
self.api_tool = tool
|
||||
# Set initial state
|
||||
self.update()
|
||||
_LOGGER.debug("Created OctoPrint binary sensor %r", self)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self.is_on
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if binary sensor is on."""
|
||||
if self._state:
|
||||
return STATE_ON
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
"""Update state of sensor."""
|
||||
try:
|
||||
self._state = self.api.update(self.sensor_type,
|
||||
self.api_endpoint,
|
||||
self.api_group,
|
||||
self.api_tool)
|
||||
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)
|
||||
@@ -7,10 +7,10 @@ at https://home-assistant.io/components/sensor.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.4']
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
@@ -48,6 +48,7 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
self.wink = wink
|
||||
self._unit_of_measurement = self.wink.UNIT
|
||||
self._battery = self.wink.battery_level
|
||||
self.capability = self.wink.capability()
|
||||
|
||||
@property
|
||||
@@ -85,3 +86,16 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
||||
def update(self):
|
||||
"""Update state of the sensor."""
|
||||
self.wink.update_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
||||
@@ -19,6 +19,8 @@ SERVICE_CONFIGURE = "configure"
|
||||
STATE_CONFIGURE = "configure"
|
||||
STATE_CONFIGURED = "configured"
|
||||
|
||||
ATTR_LINK_NAME = "link_name"
|
||||
ATTR_LINK_URL = "link_url"
|
||||
ATTR_CONFIGURE_ID = "configure_id"
|
||||
ATTR_DESCRIPTION = "description"
|
||||
ATTR_DESCRIPTION_IMAGE = "description_image"
|
||||
@@ -34,7 +36,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# pylint: disable=too-many-arguments
|
||||
def request_config(
|
||||
hass, name, callback, description=None, description_image=None,
|
||||
submit_caption=None, fields=None):
|
||||
submit_caption=None, fields=None, link_name=None, link_url=None):
|
||||
"""Create a new request for configuration.
|
||||
|
||||
Will return an ID to be used for sequent calls.
|
||||
@@ -43,7 +45,8 @@ def request_config(
|
||||
|
||||
request_id = instance.request_config(
|
||||
name, callback,
|
||||
description, description_image, submit_caption, fields)
|
||||
description, description_image, submit_caption,
|
||||
fields, link_name, link_url)
|
||||
|
||||
_REQUESTS[request_id] = instance
|
||||
|
||||
@@ -100,7 +103,8 @@ class Configurator(object):
|
||||
# pylint: disable=too-many-arguments
|
||||
def request_config(
|
||||
self, name, callback,
|
||||
description, description_image, submit_caption, fields):
|
||||
description, description_image, submit_caption,
|
||||
fields, link_name, link_url):
|
||||
"""Setup a request for configuration."""
|
||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
|
||||
|
||||
@@ -121,6 +125,8 @@ class Configurator(object):
|
||||
(ATTR_DESCRIPTION, description),
|
||||
(ATTR_DESCRIPTION_IMAGE, description_image),
|
||||
(ATTR_SUBMIT_CAPTION, submit_caption),
|
||||
(ATTR_LINK_NAME, link_name),
|
||||
(ATTR_LINK_URL, link_url),
|
||||
] if value is not None
|
||||
})
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'camera',
|
||||
'device_tracker',
|
||||
'garage_door',
|
||||
'hvac',
|
||||
'light',
|
||||
'lock',
|
||||
'media_player',
|
||||
|
||||
@@ -19,13 +19,16 @@ from homeassistant.util import Throttle
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
|
||||
_LEASES_CMD = 'cat /var/lib/misc/dnsmasq.leases'
|
||||
_LEASES_REGEX = re.compile(
|
||||
r'\w+\s' +
|
||||
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
|
||||
r'(?P<host>([^\s]+))')
|
||||
|
||||
_IP_NEIGH_CMD = 'ip neigh'
|
||||
_IP_NEIGH_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
|
||||
r'\w+\s' +
|
||||
@@ -55,6 +58,7 @@ class AsusWrtDeviceScanner(object):
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = str(config[CONF_USERNAME])
|
||||
self.password = str(config[CONF_PASSWORD])
|
||||
self.protocol = config.get('protocol')
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
@@ -100,8 +104,26 @@ class AsusWrtDeviceScanner(object):
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
def get_asuswrt_data(self):
|
||||
"""Retrieve data from ASUSWRT and return parsed result."""
|
||||
def ssh_connection(self):
|
||||
"""Retrieve data from ASUSWRT via the ssh protocol."""
|
||||
from pexpect import pxssh
|
||||
try:
|
||||
ssh = pxssh.pxssh()
|
||||
ssh.login(self.host, self.username, self.password)
|
||||
ssh.sendline(_IP_NEIGH_CMD)
|
||||
ssh.prompt()
|
||||
neighbors = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.sendline(_LEASES_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.logout()
|
||||
return (neighbors, leases_result)
|
||||
except pxssh.ExceptionPxssh as exc:
|
||||
_LOGGER.exception('Unexpected response from router: %s', exc)
|
||||
return ('', '')
|
||||
|
||||
def telnet_connection(self):
|
||||
"""Retrieve data from ASUSWRT via the telnet protocol."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'login: ')
|
||||
@@ -109,18 +131,26 @@ class AsusWrtDeviceScanner(object):
|
||||
telnet.read_until(b'Password: ')
|
||||
telnet.write((self.password + '\n').encode('ascii'))
|
||||
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
|
||||
telnet.write('ip neigh\n'.encode('ascii'))
|
||||
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
|
||||
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
|
||||
telnet.write('cat /var/lib/misc/dnsmasq.leases\n'.encode('ascii'))
|
||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
return (neighbors, leases_result)
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
return
|
||||
return ('', '')
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router," +
|
||||
_LOGGER.exception("Connection refused by router,"
|
||||
" is telnet enabled?")
|
||||
return
|
||||
return ('', '')
|
||||
|
||||
def get_asuswrt_data(self):
|
||||
"""Retrieve data from ASUSWRT and return parsed result."""
|
||||
if self.protocol == 'telnet':
|
||||
neighbors, leases_result = self.telnet_connection()
|
||||
else:
|
||||
neighbors, leases_result = self.ssh_connection()
|
||||
|
||||
devices = {}
|
||||
for lease in leases_result:
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyicloud==0.7.2']
|
||||
REQUIREMENTS = ['pyicloud==0.8.3']
|
||||
|
||||
CONF_INTERVAL = 'interval'
|
||||
DEFAULT_INTERVAL = 8
|
||||
|
||||
@@ -11,7 +11,7 @@ from collections import defaultdict
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.util import convert, slugify
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
@@ -53,6 +53,12 @@ def setup_scanner(hass, config, see):
|
||||
'accuracy %s is not met: %s',
|
||||
data_type, max_gps_accuracy, data)
|
||||
return None
|
||||
if convert(data.get('acc'), float, 1.0) == 0.0:
|
||||
_LOGGER.debug('Skipping %s update because GPS accuracy'
|
||||
'is zero',
|
||||
data_type)
|
||||
return None
|
||||
|
||||
return data
|
||||
|
||||
def owntracks_location_update(topic, payload, qos):
|
||||
@@ -91,7 +97,7 @@ def setup_scanner(hass, config, see):
|
||||
return
|
||||
# OwnTracks uses - at the start of a beacon zone
|
||||
# to switch on 'hold mode' - ignore this
|
||||
location = data['desc'].lstrip("-")
|
||||
location = slugify(data['desc'].lstrip("-"))
|
||||
if location.lower() == 'home':
|
||||
location = STATE_HOME
|
||||
|
||||
@@ -101,7 +107,7 @@ def setup_scanner(hass, config, see):
|
||||
"""Execute enter event."""
|
||||
zone = hass.states.get("zone.{}".format(location))
|
||||
with LOCK:
|
||||
if zone is None and data['t'] == 'b':
|
||||
if zone is None and data.get('t') == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location not in beacons:
|
||||
|
||||
@@ -12,7 +12,8 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
# Unifi package doesn't list urllib3 as a requirement
|
||||
REQUIREMENTS = ['urllib3', 'unifi==1.2.4']
|
||||
REQUIREMENTS = ['urllib3', 'unifi==1.2.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
|
||||
|
||||
@@ -15,10 +15,12 @@ from homeassistant.const import (
|
||||
EVENT_PLATFORM_DISCOVERED)
|
||||
|
||||
DOMAIN = "discovery"
|
||||
REQUIREMENTS = ['netdisco==0.6.4']
|
||||
REQUIREMENTS = ['netdisco==0.6.7']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
LOAD_PLATFORM = 'load_platform'
|
||||
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_CAST = 'google_cast'
|
||||
@@ -27,6 +29,7 @@ SERVICE_SONOS = 'sonos'
|
||||
SERVICE_PLEX = 'plex_mediaserver'
|
||||
SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
|
||||
SERVICE_PANASONIC_VIERA = 'panasonic_viera'
|
||||
SERVICE_ROKU = 'roku'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_WEMO: "wemo",
|
||||
@@ -37,6 +40,7 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_PLEX: 'media_player',
|
||||
SERVICE_SQUEEZEBOX: 'media_player',
|
||||
SERVICE_PANASONIC_VIERA: 'media_player',
|
||||
SERVICE_ROKU: 'media_player',
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +56,7 @@ def listen(hass, service, callback):
|
||||
|
||||
def discovery_event_listener(event):
|
||||
"""Listen for discovery events."""
|
||||
if event.data[ATTR_SERVICE] in service:
|
||||
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
|
||||
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
|
||||
|
||||
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
|
||||
@@ -73,6 +77,32 @@ def discover(hass, service, discovered=None, component=None, hass_config=None):
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
|
||||
|
||||
|
||||
def load_platform(hass, component, platform, info=None, hass_config=None):
|
||||
"""Helper method for generic platform loading.
|
||||
|
||||
This method allows a platform to be loaded dynamically without it being
|
||||
known at runtime (in the DISCOVERY_PLATFORMS list of the component).
|
||||
Advantages of using this method:
|
||||
- Any component & platforms combination can be dynamically added
|
||||
- A component (i.e. light) does not have to import every component
|
||||
that can dynamically add a platform (e.g. wemo, wink, insteon_hub)
|
||||
- Custom user components can take advantage of discovery/loading
|
||||
|
||||
Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
|
||||
fired to load the platform. The event will contain:
|
||||
{ ATTR_SERVICE = LOAD_PLATFORM + '.' + <<component>>
|
||||
ATTR_DISCOVERED = {LOAD_PLATFORM: <<platform>>} }
|
||||
|
||||
* dev note: This listener can be found in entity_component.py
|
||||
"""
|
||||
if info is None:
|
||||
info = {LOAD_PLATFORM: platform}
|
||||
else:
|
||||
info[LOAD_PLATFORM] = platform
|
||||
discover(hass, LOAD_PLATFORM + '.' + component, info, component,
|
||||
hass_config)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Start a discovery service."""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
A component which allows you to send data to Dweet.io.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/dweet/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNKNOWN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import state as state_helper
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "dweet"
|
||||
DEPENDENCIES = []
|
||||
|
||||
REQUIREMENTS = ['dweepy==0.2.0']
|
||||
|
||||
CONF_NAME = 'name'
|
||||
CONF_WHITELIST = 'whitelist'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_WHITELIST): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def setup(hass, config):
|
||||
"""Setup the Dweet.io component."""
|
||||
conf = config[DOMAIN]
|
||||
name = conf[CONF_NAME]
|
||||
whitelist = conf.get(CONF_WHITELIST, [])
|
||||
json_body = {}
|
||||
|
||||
def dweet_event_listener(event):
|
||||
"""Listen for new messages on the bus and sends them to Dweet.io."""
|
||||
state = event.data.get('new_state')
|
||||
if state is None or state.state in (STATE_UNKNOWN, '') \
|
||||
or state.entity_id not in whitelist:
|
||||
return
|
||||
|
||||
try:
|
||||
_state = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
_state = state.state
|
||||
|
||||
json_body[state.attributes.get('friendly_name')] = _state
|
||||
|
||||
send_data(name, json_body)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, dweet_event_listener)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def send_data(name, msg):
|
||||
"""Send the collected data to Dweet.io."""
|
||||
import dweepy
|
||||
try:
|
||||
dweepy.dweet_for(name, msg)
|
||||
except dweepy.DweepyError:
|
||||
_LOGGER.error("Error saving data '%s' to Dweet.io", msg)
|
||||
@@ -22,7 +22,7 @@ HOLD_TEMP = 'hold_temp'
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/nkgilley/python-ecobee-api/archive/'
|
||||
'92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4']
|
||||
'4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"""RSS/Atom feed reader for Home Assistant."""
|
||||
"""
|
||||
Support for RSS/Atom feed.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/feedreader/
|
||||
"""
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
REQUIREMENTS = ['feedparser==5.2.1']
|
||||
@@ -14,6 +20,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
'urls': [vol.Url()],
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
MAX_ENTRIES = 20
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
@@ -25,52 +32,75 @@ class FeedManager(object):
|
||||
self._url = url
|
||||
self._feed = None
|
||||
self._hass = hass
|
||||
self._firstrun = True
|
||||
# Initialize last entry timestamp as epoch time
|
||||
self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple()
|
||||
_LOGGER.debug('Loading feed %s', self._url)
|
||||
self._update()
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||
lambda _: self._update())
|
||||
track_utc_time_change(hass, lambda now: self._update(),
|
||||
minute=0, second=0)
|
||||
|
||||
def _log_no_entries(self):
|
||||
"""Send no entries log at debug level."""
|
||||
_LOGGER.debug('No new entries in feed %s', self._url)
|
||||
_LOGGER.debug('No new entries in feed "%s"', self._url)
|
||||
|
||||
def _update(self):
|
||||
"""Update the feed and publish new entries in the event bus."""
|
||||
"""Update the feed and publish new entries to the event bus."""
|
||||
import feedparser
|
||||
_LOGGER.info('Fetching new data from feed %s', self._url)
|
||||
_LOGGER.info('Fetching new data from feed "%s"', self._url)
|
||||
self._feed = feedparser.parse(self._url,
|
||||
etag=None if not self._feed
|
||||
else self._feed.get('etag'),
|
||||
modified=None if not self._feed
|
||||
else self._feed.get('modified'))
|
||||
if not self._feed:
|
||||
_LOGGER.error('Error fetching feed data from %s', self._url)
|
||||
_LOGGER.error('Error fetching feed data from "%s"', self._url)
|
||||
else:
|
||||
if self._feed.bozo != 0:
|
||||
_LOGGER.error('Error parsing feed %s', self._url)
|
||||
_LOGGER.error('Error parsing feed "%s"', self._url)
|
||||
# Using etag and modified, if there's no new data available,
|
||||
# the entries list will be empty
|
||||
elif len(self._feed.entries) > 0:
|
||||
_LOGGER.debug('Entries available in feed %s', self._url)
|
||||
_LOGGER.debug('%s entri(es) available in feed "%s"',
|
||||
len(self._feed.entries),
|
||||
self._url)
|
||||
if len(self._feed.entries) > MAX_ENTRIES:
|
||||
_LOGGER.debug('Publishing only the first %s entries '
|
||||
'in feed "%s"', MAX_ENTRIES, self._url)
|
||||
self._feed.entries = self._feed.entries[0:MAX_ENTRIES]
|
||||
self._publish_new_entries()
|
||||
self._last_entry_timestamp = \
|
||||
self._feed.entries[0].published_parsed
|
||||
else:
|
||||
self._log_no_entries()
|
||||
_LOGGER.info('Fetch from feed "%s" completed', self._url)
|
||||
|
||||
def _update_and_fire_entry(self, entry):
|
||||
"""Update last_entry_timestamp and fire entry."""
|
||||
# We are lucky, `published_parsed` data available,
|
||||
# let's make use of it to publish only new available
|
||||
# entries since the last run
|
||||
if 'published_parsed' in entry.keys():
|
||||
self._last_entry_timestamp = max(entry.published_parsed,
|
||||
self._last_entry_timestamp)
|
||||
else:
|
||||
_LOGGER.debug('No `published_parsed` info available '
|
||||
'for entry "%s"', entry.title)
|
||||
entry.update({'feed_url': self._url})
|
||||
self._hass.bus.fire(EVENT_FEEDREADER, entry)
|
||||
|
||||
def _publish_new_entries(self):
|
||||
"""Publish new entries to the event bus."""
|
||||
new_entries = False
|
||||
for entry in self._feed.entries:
|
||||
# Consider only entries newer then the latest parsed one
|
||||
if entry.published_parsed > self._last_entry_timestamp:
|
||||
if self._firstrun or (
|
||||
'published_parsed' in entry.keys() and
|
||||
entry.published_parsed > self._last_entry_timestamp):
|
||||
self._update_and_fire_entry(entry)
|
||||
new_entries = True
|
||||
entry.update({'feed_url': self._url})
|
||||
self._hass.bus.fire(EVENT_FEEDREADER, entry)
|
||||
else:
|
||||
_LOGGER.debug('Entry "%s" already processed', entry.title)
|
||||
if not new_entries:
|
||||
self._log_no_entries()
|
||||
self._firstrun = False
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
||||
@@ -28,20 +28,55 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin-bottom: 123px;
|
||||
margin-bottom: 97px;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 0pt;
|
||||
transition: font-size 2s;
|
||||
}
|
||||
|
||||
#ha-init-skeleton paper-spinner {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton a {
|
||||
color: #03A9F4;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#ha-init-skeleton.error {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton.error img,
|
||||
#ha-init-skeleton.error paper-spinner {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<link rel='import' href='/static/{{ app_url }}' async>
|
||||
<script>
|
||||
function initError() {
|
||||
document
|
||||
.getElementById('ha-init-skeleton')
|
||||
.classList.add('error');
|
||||
}
|
||||
</script>
|
||||
<link rel='import' href='/static/{{ app_url }}' onerror='initError()' async>
|
||||
</head>
|
||||
<body fullbleed>
|
||||
<div id='ha-init-skeleton'><img src='/static/favicon-192x192.png' height='192'></div>
|
||||
<div id='ha-init-skeleton'>
|
||||
<img src='/static/favicon-192x192.png' height='192'>
|
||||
<paper-spinner active></paper-spinner>
|
||||
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
|
||||
</div>
|
||||
<script>
|
||||
var webComponentsSupported = ('registerElement' in document &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'))
|
||||
var webComponentsSupported = (
|
||||
'registerElement' in document &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
var script = document.createElement('script')
|
||||
script.async = true
|
||||
script.onerror = initError;
|
||||
script.src = '/static/webcomponents-lite.min.js'
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
|
||||
VERSION = "af8a531f1c2e477c07c4b3394bd1ce13"
|
||||
VERSION = "1baebe8155deb447230866d7ae854bd9"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
||||
VERSION = "ffd8a1bde5ba13f300c3d6ad32036526"
|
||||
VERSION = "0a226e905af198b2dabf1ce154844568"
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
File diff suppressed because one or more lines are too long
@@ -1,5 +1 @@
|
||||
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
|
||||
!*** ./src/service-worker/index.js ***!
|
||||
\*************************************/
|
||||
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}]);
|
||||
//# sourceMappingURL=service_worker.js.map
|
||||
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={i:r,l:!1,exports:{}};return e[r].call(s.exports,s,s.exports,t),s.l=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(t.s=194)}({194:function(e,t,n){var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}});
|
||||
File diff suppressed because one or more lines are too long
@@ -7,9 +7,9 @@ https://home-assistant.io/components/garage_door.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.4']
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -37,6 +37,7 @@ class WinkGarageDoorDevice(GarageDoorDevice):
|
||||
def __init__(self, wink):
|
||||
"""Initialize the garage door."""
|
||||
self.wink = wink
|
||||
self._battery = self.wink.battery_level
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -69,3 +70,16 @@ class WinkGarageDoorDevice(GarageDoorDevice):
|
||||
def open_door(self):
|
||||
"""Open the door."""
|
||||
self.wink.set_state(1)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
||||
@@ -16,18 +16,23 @@ from http import cookies
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_ACCEPT_ENCODING,
|
||||
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONTENT_ENCODING,
|
||||
HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_EXPIRES,
|
||||
HTTP_HEADER_HA_AUTH, HTTP_HEADER_VARY, HTTP_METHOD_NOT_ALLOWED,
|
||||
HTTP_HEADER_HA_AUTH, HTTP_HEADER_VARY,
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, HTTP_METHOD_NOT_ALLOWED,
|
||||
HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_UNPROCESSABLE_ENTITY,
|
||||
ALLOWED_CORS_HEADERS,
|
||||
SERVER_PORT, URL_ROOT, URL_API_EVENT_FORWARD)
|
||||
|
||||
DOMAIN = "http"
|
||||
@@ -38,6 +43,7 @@ CONF_SERVER_PORT = "server_port"
|
||||
CONF_DEVELOPMENT = "development"
|
||||
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
||||
CONF_SSL_KEY = 'ssl_key'
|
||||
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
|
||||
@@ -48,6 +54,19 @@ SESSION_KEY = 'sessionId'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_API_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SERVER_HOST): cv.string,
|
||||
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_DEVELOPMENT): cv.string,
|
||||
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
|
||||
vol.Optional(CONF_SSL_KEY): cv.isfile,
|
||||
vol.Optional(CONF_CORS_ORIGINS): cv.ensure_list
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the HTTP API and debug interface."""
|
||||
@@ -61,11 +80,12 @@ def setup(hass, config):
|
||||
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
|
||||
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
||||
ssl_key = conf.get(CONF_SSL_KEY)
|
||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||
|
||||
try:
|
||||
server = HomeAssistantHTTPServer(
|
||||
(server_host, server_port), RequestHandler, hass, api_password,
|
||||
development, ssl_certificate, ssl_key)
|
||||
development, ssl_certificate, ssl_key, cors_origins)
|
||||
except OSError:
|
||||
# If address already in use
|
||||
_LOGGER.exception("Error setting up HTTP server")
|
||||
@@ -96,7 +116,8 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, server_address, request_handler_class,
|
||||
hass, api_password, development, ssl_certificate, ssl_key):
|
||||
hass, api_password, development, ssl_certificate, ssl_key,
|
||||
cors_origins):
|
||||
"""Initialize the server."""
|
||||
super().__init__(server_address, request_handler_class)
|
||||
|
||||
@@ -107,6 +128,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
self.paths = []
|
||||
self.sessions = SessionStore()
|
||||
self.use_ssl = ssl_certificate is not None
|
||||
self.cors_origins = cors_origins
|
||||
|
||||
# We will lazy init this one if needed
|
||||
self.event_forwarder = None
|
||||
@@ -351,6 +373,16 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING)
|
||||
|
||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(content)))
|
||||
|
||||
cors_check = (self.headers.get("Origin") in self.server.cors_origins)
|
||||
|
||||
cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
|
||||
|
||||
if self.server.cors_origins and cors_check:
|
||||
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
self.headers.get("Origin"))
|
||||
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
cors_headers)
|
||||
self.end_headers()
|
||||
|
||||
if self.command == 'HEAD':
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
"""
|
||||
Provides functionality to interact with hvacs.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/hvac/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.temperature import convert
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
TEMP_CELCIUS)
|
||||
|
||||
DOMAIN = "hvac"
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
SCAN_INTERVAL = 60
|
||||
|
||||
SERVICE_SET_AWAY_MODE = "set_away_mode"
|
||||
SERVICE_SET_AUX_HEAT = "set_aux_heat"
|
||||
SERVICE_SET_TEMPERATURE = "set_temperature"
|
||||
SERVICE_SET_FAN_MODE = "set_fan_mode"
|
||||
SERVICE_SET_OPERATION_MODE = "set_operation_mode"
|
||||
SERVICE_SET_SWING_MODE = "set_swing_mode"
|
||||
SERVICE_SET_HUMIDITY = "set_humidity"
|
||||
|
||||
STATE_HEAT = "heat"
|
||||
STATE_COOL = "cool"
|
||||
STATE_IDLE = "idle"
|
||||
STATE_AUTO = "auto"
|
||||
STATE_DRY = "dry"
|
||||
STATE_FAN_ONLY = "fan_only"
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = "current_temperature"
|
||||
ATTR_MAX_TEMP = "max_temp"
|
||||
ATTR_MIN_TEMP = "min_temp"
|
||||
ATTR_AWAY_MODE = "away_mode"
|
||||
ATTR_AUX_HEAT = "aux_heat"
|
||||
ATTR_FAN_MODE = "fan_mode"
|
||||
ATTR_FAN_LIST = "fan_list"
|
||||
ATTR_CURRENT_HUMIDITY = "current_humidity"
|
||||
ATTR_HUMIDITY = "humidity"
|
||||
ATTR_MAX_HUMIDITY = "max_humidity"
|
||||
ATTR_MIN_HUMIDITY = "min_humidity"
|
||||
ATTR_OPERATION_MODE = "operation_mode"
|
||||
ATTR_OPERATION_LIST = "operation_list"
|
||||
ATTR_SWING_MODE = "swing_mode"
|
||||
ATTR_SWING_LIST = "swing_list"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
zwave.DISCOVER_HVAC: 'zwave'
|
||||
}
|
||||
|
||||
|
||||
def set_away_mode(hass, away_mode, entity_id=None):
|
||||
"""Turn all or specified hvac away mode on."""
|
||||
data = {
|
||||
ATTR_AWAY_MODE: away_mode
|
||||
}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
|
||||
|
||||
|
||||
def set_aux_heat(hass, aux_heat, entity_id=None):
|
||||
"""Turn all or specified hvac auxillary heater on."""
|
||||
data = {
|
||||
ATTR_AUX_HEAT: aux_heat
|
||||
}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
||||
|
||||
|
||||
def set_temperature(hass, temperature, entity_id=None):
|
||||
"""Set new target temperature."""
|
||||
data = {ATTR_TEMPERATURE: temperature}
|
||||
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data)
|
||||
|
||||
|
||||
def set_humidity(hass, humidity, entity_id=None):
|
||||
"""Set new target humidity."""
|
||||
data = {ATTR_HUMIDITY: humidity}
|
||||
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data)
|
||||
|
||||
|
||||
def set_fan_mode(hass, fan, entity_id=None):
|
||||
"""Turn all or specified hvac fan mode on."""
|
||||
data = {ATTR_FAN_MODE: fan}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
|
||||
|
||||
|
||||
def set_operation_mode(hass, operation_mode, entity_id=None):
|
||||
"""Set new target operation mode."""
|
||||
data = {ATTR_OPERATION_MODE: operation_mode}
|
||||
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)
|
||||
|
||||
|
||||
def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
"""Set new target swing mode."""
|
||||
data = {ATTR_SWING_MODE: swing_mode}
|
||||
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
"""Setup hvacs."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
SCAN_INTERVAL, DISCOVERY_PLATFORMS)
|
||||
component.setup(config)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def away_mode_set_service(service):
|
||||
"""Set away mode on target hvacs."""
|
||||
target_hvacs = component.extract_from_service(service)
|
||||
|
||||
away_mode = service.data.get(ATTR_AWAY_MODE)
|
||||
|
||||
if away_mode is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
if away_mode:
|
||||
hvac.turn_away_mode_on()
|
||||
else:
|
||||
hvac.turn_away_mode_off()
|
||||
|
||||
if hvac.should_poll:
|
||||
hvac.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_AWAY_MODE))
|
||||
|
||||
def aux_heat_set_service(service):
|
||||
"""Set auxillary heater on target hvacs."""
|
||||
target_hvacs = component.extract_from_service(service)
|
||||
|
||||
aux_heat = service.data.get(ATTR_AUX_HEAT)
|
||||
|
||||
if aux_heat is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
if aux_heat:
|
||||
hvac.turn_aux_heat_on()
|
||||
else:
|
||||
hvac.turn_aux_heat_off()
|
||||
|
||||
if hvac.should_poll:
|
||||
hvac.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service,
|
||||
descriptions.get(SERVICE_SET_AUX_HEAT))
|
||||
|
||||
def temperature_set_service(service):
|
||||
"""Set temperature on the target hvacs."""
|
||||
target_hvacs = component.extract_from_service(service)
|
||||
|
||||
temperature = util.convert(
|
||||
service.data.get(ATTR_TEMPERATURE), float)
|
||||
|
||||
if temperature is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
hvac.set_temperature(convert(
|
||||
temperature, hass.config.temperature_unit,
|
||||
hvac.unit_of_measurement))
|
||||
|
||||
if hvac.should_poll:
|
||||
hvac.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service,
|
||||
descriptions.get(SERVICE_SET_TEMPERATURE))
|
||||
|
||||
def humidity_set_service(service):
|
||||
"""Set humidity on the target hvacs."""
|
||||
target_hvacs = component.extract_from_service(service)
|
||||
|
||||
humidity = service.data.get(ATTR_HUMIDITY)
|
||||
|
||||
if humidity is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_HUMIDITY, ATTR_HUMIDITY)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
hvac.set_humidity(humidity)
|
||||
|
||||
if hvac.should_poll:
|
||||
hvac.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service,
|
||||
descriptions.get(SERVICE_SET_HUMIDITY))
|
||||
|
||||
def fan_mode_set_service(service):
|
||||
"""Set fan mode on target hvacs."""
|
||||
target_hvacs = component.extract_from_service(service)
|
||||
|
||||
fan = service.data.get(ATTR_FAN_MODE)
|
||||
|
||||
if fan is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_FAN_MODE, ATTR_FAN_MODE)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
hvac.set_fan_mode(fan)
|
||||
|
||||
if hvac.should_poll:
|
||||
hvac.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service,
|
||||
descriptions.get(SERVICE_SET_FAN_MODE))
|
||||
|
||||
def operation_set_service(service):
|
||||
"""Set operating mode on the target hvacs."""
|
||||
target_hvacs = component.extract_from_service(service)
|
||||
|
||||
operation_mode = service.data.get(ATTR_OPERATION_MODE)
|
||||
|
||||
if operation_mode is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
hvac.set_operation_mode(operation_mode)
|
||||
|
||||
if hvac.should_poll:
|
||||
hvac.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service,
|
||||
descriptions.get(SERVICE_SET_OPERATION_MODE))
|
||||
|
||||
def swing_set_service(service):
|
||||
"""Set swing mode on the target hvacs."""
|
||||
target_hvacs = component.extract_from_service(service)
|
||||
|
||||
swing_mode = service.data.get(ATTR_SWING_MODE)
|
||||
|
||||
if swing_mode is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_SWING_MODE, ATTR_SWING_MODE)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
hvac.set_swing_mode(swing_mode)
|
||||
|
||||
if hvac.should_poll:
|
||||
hvac.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
|
||||
descriptions.get(SERVICE_SET_SWING_MODE))
|
||||
return True
|
||||
|
||||
|
||||
class HvacDevice(Entity):
|
||||
"""Representation of a hvac."""
|
||||
|
||||
# pylint: disable=too-many-public-methods,no-self-use
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
return self.current_operation or STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {
|
||||
ATTR_CURRENT_TEMPERATURE:
|
||||
self._convert_for_display(self.current_temperature),
|
||||
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp),
|
||||
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
|
||||
ATTR_TEMPERATURE:
|
||||
self._convert_for_display(self.target_temperature),
|
||||
}
|
||||
|
||||
humidity = self.target_humidity
|
||||
if humidity is not None:
|
||||
data[ATTR_HUMIDITY] = humidity
|
||||
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
|
||||
data[ATTR_MIN_HUMIDITY] = self.min_humidity
|
||||
data[ATTR_MAX_HUMIDITY] = self.max_humidity
|
||||
|
||||
fan_mode = self.current_fan_mode
|
||||
if fan_mode is not None:
|
||||
data[ATTR_FAN_MODE] = fan_mode
|
||||
data[ATTR_FAN_LIST] = self.fan_list
|
||||
|
||||
operation_mode = self.current_operation
|
||||
if operation_mode is not None:
|
||||
data[ATTR_OPERATION_MODE] = operation_mode
|
||||
data[ATTR_OPERATION_LIST] = self.operation_list
|
||||
|
||||
swing_mode = self.current_swing_mode
|
||||
if swing_mode is not None:
|
||||
data[ATTR_SWING_MODE] = swing_mode
|
||||
data[ATTR_SWING_LIST] = self.swing_list
|
||||
|
||||
is_away = self.is_away_mode_on
|
||||
if is_away is not None:
|
||||
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
|
||||
|
||||
is_aux_heat = self.is_aux_heat_on
|
||||
if is_aux_heat is not None:
|
||||
data[ATTR_AUX_HEAT] = STATE_ON if is_aux_heat else STATE_OFF
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_humidity(self):
|
||||
"""Return the humidity we try to reach."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return None
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
pass
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set new target humidity."""
|
||||
pass
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
pass
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
pass
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
pass
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
pass
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
pass
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
pass
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return convert(7, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return convert(35, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
|
||||
@property
|
||||
def min_humidity(self):
|
||||
"""Return the minimum humidity."""
|
||||
return 30
|
||||
|
||||
@property
|
||||
def max_humidity(self):
|
||||
"""Return the maximum humidity."""
|
||||
return 99
|
||||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if temp is None:
|
||||
return None
|
||||
|
||||
value = convert(temp, self.unit_of_measurement,
|
||||
self.hass.config.temperature_unit)
|
||||
|
||||
if self.hass.config.temperature_unit is TEMP_CELCIUS:
|
||||
decimal_count = 1
|
||||
else:
|
||||
# Users of fahrenheit generally expect integer units.
|
||||
decimal_count = 0
|
||||
|
||||
return round(value, decimal_count)
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Demo platform that offers a fake hvac.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
from homeassistant.components.hvac import HvacDevice
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo hvacs."""
|
||||
add_devices([
|
||||
DemoHvac("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low",
|
||||
None, None, "Auto", "Heat", None),
|
||||
DemoHvac("Hvac", 21, TEMP_CELSIUS, True, 22, "On High",
|
||||
67, 54, "Off", "Cool", False),
|
||||
])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-public-methods
|
||||
class DemoHvac(HvacDevice):
|
||||
"""Representation of a demo hvac."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, name, target_temperature, unit_of_measurement,
|
||||
away, current_temperature, current_fan_mode,
|
||||
target_humidity, current_humidity, current_swing_mode,
|
||||
current_operation, aux):
|
||||
"""Initialize the hvac."""
|
||||
self._name = name
|
||||
self._target_temperature = target_temperature
|
||||
self._target_humidity = target_humidity
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._away = away
|
||||
self._current_temperature = current_temperature
|
||||
self._current_humidity = current_humidity
|
||||
self._current_fan_mode = current_fan_mode
|
||||
self._current_operation = current_operation
|
||||
self._aux = aux
|
||||
self._current_swing_mode = current_swing_mode
|
||||
self._fan_list = ["On Low", "On High", "Auto Low", "Auto High", "Off"]
|
||||
self._operation_list = ["Heat", "Cool", "Auto Changeover", "Off"]
|
||||
self._swing_list = ["Auto", 1, 2, 3, "Off"]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling not needed for a demo hvac."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the hvac."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._current_humidity
|
||||
|
||||
@property
|
||||
def target_humidity(self):
|
||||
"""Return the humidity we try to reach."""
|
||||
return self._target_humidity
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if away mode is on."""
|
||||
return self._away
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._aux
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
self._target_temperature = temperature
|
||||
self.update_ha_state()
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set new target temperature."""
|
||||
self._target_humidity = humidity
|
||||
self.update_ha_state()
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target temperature."""
|
||||
self._current_swing_mode = swing_mode
|
||||
self.update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target temperature."""
|
||||
self._current_fan_mode = fan
|
||||
self.update_ha_state()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target temperature."""
|
||||
self._current_operation = operation_mode
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the swing setting."""
|
||||
return self._current_swing_mode
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return self._swing_list
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
self._away = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn away auxillary heater on."""
|
||||
self._aux = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
self._aux = False
|
||||
self.update_ha_state()
|
||||
@@ -0,0 +1,84 @@
|
||||
set_aux_heat:
|
||||
description: Turn auxillary heater on/off for hvac
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'hvac.kitchen'
|
||||
|
||||
aux_heat:
|
||||
description: New value of axillary heater
|
||||
example: true
|
||||
|
||||
set_away_mode:
|
||||
description: Turn away mode on/off for hvac
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'hvac.kitchen'
|
||||
|
||||
away_mode:
|
||||
description: New value of away mode
|
||||
example: true
|
||||
|
||||
set_temperature:
|
||||
description: Set target temperature of hvac
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'hvac.kitchen'
|
||||
|
||||
temperature:
|
||||
description: New target temperature for hvac
|
||||
example: 25
|
||||
|
||||
set_humidity:
|
||||
description: Set target humidity of hvac
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'hvac.kitchen'
|
||||
|
||||
humidity:
|
||||
description: New target humidity for hvac
|
||||
example: 60
|
||||
|
||||
set_fan_mode:
|
||||
description: Set fan operation for hvac
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'hvac.nest'
|
||||
|
||||
fan:
|
||||
description: New value of fan mode
|
||||
example: On Low
|
||||
|
||||
set_operation_mode:
|
||||
description: Set operation mode for hvac
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'hvac.nest'
|
||||
|
||||
operation_mode:
|
||||
description: New value of operation mode
|
||||
example: Heat
|
||||
|
||||
|
||||
set_swing_mode:
|
||||
description: Set swing operation for hvac
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: 'hvac.nest'
|
||||
|
||||
swing_mode:
|
||||
description: New value of swing mode
|
||||
example: 1
|
||||
Executable
+245
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
Support for ZWave HVAC devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/hvac.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.hvac import DOMAIN
|
||||
from homeassistant.components.hvac import HvacDevice
|
||||
from homeassistant.components.zwave import (
|
||||
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'ZWave Hvac'
|
||||
|
||||
REMOTEC = 0x5254
|
||||
REMOTEC_ZXT_120 = 0x8377
|
||||
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0)
|
||||
|
||||
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
|
||||
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
|
||||
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
|
||||
COMMAND_CLASS_CONFIGURATION = 0x70
|
||||
|
||||
WORKAROUND_ZXT_120 = 'zxt_120'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
|
||||
}
|
||||
|
||||
ZXT_120_SET_TEMP = {
|
||||
'Heat': 1,
|
||||
'Cool': 2,
|
||||
'Dry Air': 8,
|
||||
'Auto Changeover': 10
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the ZWave Hvac devices."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZWaveHvac(value)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
"""Represents a HeatControl hvac."""
|
||||
|
||||
# pylint: disable=too-many-public-methods, too-many-instance-attributes
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave hvac."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._node = value.node
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_operation = None
|
||||
self._operation_list = None
|
||||
self._current_operation_state = None
|
||||
self._current_fan_mode = None
|
||||
self._fan_list = None
|
||||
self._current_swing_mode = None
|
||||
self._swing_list = None
|
||||
self._unit = None
|
||||
self._zxt_120 = None
|
||||
self.update_properties()
|
||||
# register listener
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16),
|
||||
value.index)
|
||||
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
|
||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat as HVAC")
|
||||
self._zxt_120 = 1
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.node == value.node:
|
||||
self.update_properties()
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# Set point
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
if int(value.data) != 0:
|
||||
self._target_temperature = int(value.data)
|
||||
# Operation Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
self._current_operation = value.data
|
||||
self._operation_list = list(value.data_items)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
# Current Temp
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
|
||||
self._current_temperature = int(value.data)
|
||||
self._unit = value.units
|
||||
# Fan Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
self._current_operation_state = value.data
|
||||
self._fan_list = list(value.data_items)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
_LOGGER.debug("self._current_operation_state=%s",
|
||||
self._current_operation_state)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
self._current_swing_mode = value.data
|
||||
self._swing_list = [0, 1]
|
||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling on ZWave."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan speed set."""
|
||||
return self._current_operation_state
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the swing mode set."""
|
||||
return self._current_swing_mode
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return self._swing_list
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
unit = self._unit
|
||||
if unit == 'C':
|
||||
return TEMP_CELSIUS
|
||||
elif unit == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
_LOGGER.exception("unit_of_measurement=%s is not valid",
|
||||
unit)
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the current operation mode."""
|
||||
return self._current_operation
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
if value.command_class != 67:
|
||||
continue
|
||||
if self._zxt_120:
|
||||
# ZXT-120 does not support get setpoint
|
||||
self._target_temperature = temperature
|
||||
if ZXT_120_SET_TEMP.get(self._current_operation) \
|
||||
!= value.index:
|
||||
continue
|
||||
# ZXT-120 responds only to whole int
|
||||
value.data = int(round(temperature, 0))
|
||||
else:
|
||||
value.data = int(temperature)
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
if value.command_class == 68 and value.index == 0:
|
||||
value.data = bytes(fan, 'utf-8')
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
if value.command_class == 64 and value.index == 0:
|
||||
value.data = bytes(operation_mode, 'utf-8')
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
if self._zxt_120 == 1:
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
value.data = int(swing_mode)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._convert_for_display(19)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._convert_for_display(30)
|
||||
@@ -39,6 +39,7 @@ ATTR_TRANSITION = "transition"
|
||||
ATTR_RGB_COLOR = "rgb_color"
|
||||
ATTR_XY_COLOR = "xy_color"
|
||||
ATTR_COLOR_TEMP = "color_temp"
|
||||
ATTR_COLOR_NAME = "color_name"
|
||||
|
||||
# int with value 0 .. 255 representing brightness of the light.
|
||||
ATTR_BRIGHTNESS = "brightness"
|
||||
@@ -87,6 +88,7 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
||||
ATTR_PROFILE: str,
|
||||
ATTR_TRANSITION: VALID_TRANSITION,
|
||||
ATTR_BRIGHTNESS: cv.byte,
|
||||
ATTR_COLOR_NAME: str,
|
||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||
vol.Coerce(tuple)),
|
||||
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||
@@ -122,7 +124,7 @@ def is_on(hass, entity_id=None):
|
||||
# pylint: disable=too-many-arguments
|
||||
def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||
rgb_color=None, xy_color=None, color_temp=None, profile=None,
|
||||
flash=None, effect=None):
|
||||
flash=None, effect=None, color_name=None):
|
||||
"""Turn all or specified light on."""
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
@@ -135,6 +137,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||
(ATTR_COLOR_TEMP, color_temp),
|
||||
(ATTR_FLASH, flash),
|
||||
(ATTR_EFFECT, effect),
|
||||
(ATTR_COLOR_NAME, color_name),
|
||||
] if value is not None
|
||||
}
|
||||
|
||||
@@ -228,6 +231,11 @@ def setup(hass, config):
|
||||
params.setdefault(ATTR_XY_COLOR, profile[:2])
|
||||
params.setdefault(ATTR_BRIGHTNESS, profile[2])
|
||||
|
||||
color_name = params.pop(ATTR_COLOR_NAME, None)
|
||||
|
||||
if color_name is not None:
|
||||
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
|
||||
|
||||
for light in target_lights:
|
||||
light.turn_on(**params)
|
||||
|
||||
|
||||
@@ -235,14 +235,16 @@ class HueLight(Light):
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
command['bri'] = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
if ATTR_XY_COLOR in kwargs:
|
||||
command['xy'] = kwargs[ATTR_XY_COLOR]
|
||||
elif ATTR_RGB_COLOR in kwargs:
|
||||
command['xy'] = color_util.color_RGB_to_xy(
|
||||
xyb = color_util.color_RGB_to_xy(
|
||||
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
|
||||
command['xy'] = xyb[0], xyb[1]
|
||||
command['bri'] = xyb[2]
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
command['bri'] = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
command['ct'] = kwargs[ATTR_COLOR_TEMP]
|
||||
|
||||
@@ -6,10 +6,10 @@ https://home-assistant.io/components/light.mysensors/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
|
||||
Light)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.util.color import rgb_hex_to_rgb_list
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -25,8 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
mysensors = get_component('mysensors')
|
||||
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||
# states. Map them in a dict of lists.
|
||||
@@ -52,35 +50,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
map_sv_types, devices, add_devices, device_class_map))
|
||||
|
||||
|
||||
class MySensorsLight(Light):
|
||||
"""Represent the value of a MySensors child node."""
|
||||
class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
||||
"""Represent the value of a MySensors Light child node."""
|
||||
|
||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||
def __init__(
|
||||
self, gateway, node_id, child_id, name, value_type, child_type):
|
||||
def __init__(self, *args):
|
||||
"""Setup instance attributes."""
|
||||
self.gateway = gateway
|
||||
self.node_id = node_id
|
||||
self.child_id = child_id
|
||||
self._name = name
|
||||
self.value_type = value_type
|
||||
self.battery_level = 0
|
||||
self._values = {}
|
||||
mysensors.MySensorsDeviceEntity.__init__(self, *args)
|
||||
self._state = None
|
||||
self._brightness = None
|
||||
self._rgb = None
|
||||
self._white = None
|
||||
self.mysensors = get_component('mysensors')
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
@@ -97,29 +76,6 @@ class MySensorsLight(Light):
|
||||
"""Return the white value in RGBW, value between 0..255."""
|
||||
return self._white
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
address = getattr(self.gateway, 'server_address', None)
|
||||
if address:
|
||||
device = '{}:{}'.format(address[0], address[1])
|
||||
else:
|
||||
device = self.gateway.port
|
||||
attr = {
|
||||
self.mysensors.ATTR_DEVICE: device,
|
||||
self.mysensors.ATTR_NODE_ID: self.node_id,
|
||||
self.mysensors.ATTR_CHILD_ID: self.child_id,
|
||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||
}
|
||||
for value_type, value in self._values.items():
|
||||
attr[self.gateway.const.SetReq(value_type).name] = value
|
||||
return attr
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true if entity is available."""
|
||||
return self.value_type in self._values
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return true if unable to access real state of entity."""
|
||||
@@ -319,28 +275,11 @@ class MySensorsLightRGB(MySensorsLight):
|
||||
self._update_rgb_or_w()
|
||||
|
||||
|
||||
class MySensorsLightRGBW(MySensorsLight):
|
||||
"""RGBW child class to MySensorsLight."""
|
||||
class MySensorsLightRGBW(MySensorsLightRGB):
|
||||
"""RGBW child class to MySensorsLightRGB."""
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
self._turn_on_light()
|
||||
self._turn_on_dimmer(**kwargs)
|
||||
self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
ret = self._turn_off_rgb_or_w()
|
||||
ret = self._turn_off_dimmer(
|
||||
value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE])
|
||||
ret = self._turn_off_light(
|
||||
value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE])
|
||||
self._turn_off_main(
|
||||
value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE])
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
self._update_main()
|
||||
self._update_light()
|
||||
self._update_dimmer()
|
||||
self._update_rgb_or_w()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Support for Qwikswitch Relays and Dimmers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.qwikswitch/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.qwikswitch as qwikswitch
|
||||
from homeassistant.components.light import Light
|
||||
|
||||
DEPENDENCIES = ['qwikswitch']
|
||||
|
||||
|
||||
class QSLight(qwikswitch.QSToggleEntity, Light):
|
||||
"""Light based on a Qwikswitch relay/dimmer module."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Store add_devices for the light components."""
|
||||
if discovery_info is None or 'qsusb_id' not in discovery_info:
|
||||
logging.getLogger(__name__).error(
|
||||
'Configure main Qwikswitch component')
|
||||
return False
|
||||
|
||||
qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']]
|
||||
|
||||
for item in qsusb.ha_devices:
|
||||
if item['type'] not in ['dim', 'rel']:
|
||||
continue
|
||||
dev = QSLight(item, qsusb)
|
||||
add_devices([dev])
|
||||
qsusb.ha_objects[item['id']] = dev
|
||||
@@ -16,6 +16,10 @@ turn_on:
|
||||
description: Color for the light in RGB-format
|
||||
example: '[255, 100, 100]'
|
||||
|
||||
color_name:
|
||||
description: A human readable color name
|
||||
example: 'red'
|
||||
|
||||
xy_color:
|
||||
description: Color for the light in XY-format
|
||||
example: '[0.52, 0.43]'
|
||||
|
||||
@@ -4,12 +4,16 @@ Support for Tellstick lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.tellstick/
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import tellstick
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||
from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS,
|
||||
ATTR_DISCOVER_DEVICES,
|
||||
ATTR_DISCOVER_CONFIG)
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
@@ -105,6 +105,7 @@ class WemoLight(Light):
|
||||
elif ATTR_RGB_COLOR in kwargs:
|
||||
xycolor = color_util.color_RGB_to_xy(
|
||||
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
|
||||
kwargs.setdefault(ATTR_BRIGHTNESS, xycolor[2])
|
||||
else:
|
||||
xycolor = None
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.util import color as color_util
|
||||
from homeassistant.util.color import \
|
||||
color_temperature_mired_to_kelvin as mired_to_kelvin
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.4']
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
@@ -97,7 +97,9 @@ class WinkLight(Light):
|
||||
}
|
||||
|
||||
if rgb_color:
|
||||
state_kwargs['color_xy'] = color_util.color_RGB_to_xy(*rgb_color)
|
||||
xyb = color_util.color_RGB_to_xy(*rgb_color)
|
||||
state_kwargs['color_xy'] = xyb[0], xyb[1]
|
||||
state_kwargs['brightness'] = xyb[2]
|
||||
|
||||
if color_temp_mired:
|
||||
state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired)
|
||||
|
||||
@@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
|
||||
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)
|
||||
from homeassistant.components import (group, verisure, wink)
|
||||
from homeassistant.components import (group, verisure, wink, zwave)
|
||||
|
||||
DOMAIN = 'lock'
|
||||
SCAN_INTERVAL = 30
|
||||
@@ -33,7 +33,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_LOCKS: 'wink',
|
||||
verisure.DISCOVER_LOCKS: 'verisure'
|
||||
verisure.DISCOVER_LOCKS: 'verisure',
|
||||
zwave.DISCOVER_LOCKS: 'zwave',
|
||||
}
|
||||
|
||||
LOCK_SERVICE_SCHEMA = vol.Schema({
|
||||
|
||||
@@ -46,6 +46,11 @@ class VerisureDoorlock(LockDevice):
|
||||
"""Return the state of the lock."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return hub.available
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the required six digit code."""
|
||||
|
||||
@@ -7,9 +7,9 @@ https://home-assistant.io/components/lock.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.4']
|
||||
REQUIREMENTS = ['python-wink==0.7.6']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -36,6 +36,7 @@ class WinkLockDevice(LockDevice):
|
||||
def __init__(self, wink):
|
||||
"""Initialize the lock."""
|
||||
self.wink = wink
|
||||
self._battery = self.wink.battery_level
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -68,3 +69,16 @@ class WinkLockDevice(LockDevice):
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
self.wink.set_state(False)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Zwave platform that handles simple door locks.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/lock.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
from homeassistant.components.lock import DOMAIN, LockDevice
|
||||
from homeassistant.components import zwave
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave switches."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_DOOR_LOCK:
|
||||
return
|
||||
if value.type != zwave.TYPE_BOOL:
|
||||
return
|
||||
if value.genre != zwave.GENRE_USER:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveLock(value)])
|
||||
|
||||
|
||||
class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
||||
"""Representation of a Z-Wave switch."""
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the Z-Wave switch device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
|
||||
self._state = value.data
|
||||
dispatcher.connect(
|
||||
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def _value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self._state = value.data
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if device is locked."""
|
||||
return self._state
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
self._value.data = True
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
self._value.data = False
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Support for sending data to Logentries webhook endpoint.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/logentries/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import EVENT_STATE_CHANGED
|
||||
from homeassistant.helpers import state as state_helper
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "logentries"
|
||||
DEPENDENCIES = []
|
||||
|
||||
DEFAULT_HOST = 'https://webhook.logentries.com/noformat/logs/'
|
||||
|
||||
CONF_TOKEN = 'token'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Logentries component."""
|
||||
if not validate_config(config, {DOMAIN: ['token']}, _LOGGER):
|
||||
_LOGGER.error("Logentries token not present")
|
||||
return False
|
||||
conf = config[DOMAIN]
|
||||
token = util.convert(conf.get(CONF_TOKEN), str)
|
||||
le_wh = DEFAULT_HOST + token
|
||||
|
||||
def logentries_event_listener(event):
|
||||
"""Listen for new messages on the bus and sends them to Logentries."""
|
||||
state = event.data.get('new_state')
|
||||
if state is None:
|
||||
return
|
||||
try:
|
||||
_state = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
_state = state.state
|
||||
json_body = [
|
||||
{
|
||||
'domain': state.domain,
|
||||
'entity_id': state.object_id,
|
||||
'attributes': dict(state.attributes),
|
||||
'time': str(event.time_fired),
|
||||
'value': _state,
|
||||
}
|
||||
]
|
||||
try:
|
||||
payload = {"host": le_wh,
|
||||
"event": json_body}
|
||||
requests.post(le_wh, data=json.dumps(payload), timeout=10)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.exception('Error sending to Logentries: %s', error)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, logentries_event_listener)
|
||||
|
||||
return True
|
||||
@@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
|
||||
SERVICE_VOLUME_MUTE, SERVICE_TOGGLE,
|
||||
SERVICE_VOLUME_MUTE, SERVICE_TOGGLE, SERVICE_MEDIA_STOP,
|
||||
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
||||
|
||||
@@ -36,6 +36,7 @@ DISCOVERY_PLATFORMS = {
|
||||
discovery.SERVICE_PLEX: 'plex',
|
||||
discovery.SERVICE_SQUEEZEBOX: 'squeezebox',
|
||||
discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera',
|
||||
discovery.SERVICE_ROKU: 'roku',
|
||||
}
|
||||
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
@@ -62,6 +63,7 @@ ATTR_APP_NAME = 'app_name'
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
|
||||
ATTR_INPUT_SOURCE = 'source'
|
||||
ATTR_INPUT_SOURCE_LIST = 'source_list'
|
||||
ATTR_MEDIA_ENQUEUE = 'enqueue'
|
||||
|
||||
MEDIA_TYPE_MUSIC = 'music'
|
||||
MEDIA_TYPE_TVSHOW = 'tvshow'
|
||||
@@ -82,6 +84,7 @@ SUPPORT_TURN_OFF = 256
|
||||
SUPPORT_PLAY_MEDIA = 512
|
||||
SUPPORT_VOLUME_STEP = 1024
|
||||
SUPPORT_SELECT_SOURCE = 2048
|
||||
SUPPORT_STOP = 4096
|
||||
|
||||
# simple services that only take entity_id(s) as optional argument
|
||||
SERVICE_TO_METHOD = {
|
||||
@@ -93,6 +96,7 @@ SERVICE_TO_METHOD = {
|
||||
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
|
||||
SERVICE_MEDIA_PLAY: 'media_play',
|
||||
SERVICE_MEDIA_PAUSE: 'media_pause',
|
||||
SERVICE_MEDIA_STOP: 'media_stop',
|
||||
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
|
||||
SERVICE_SELECT_SOURCE: 'select_source'
|
||||
@@ -142,6 +146,7 @@ MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
|
||||
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
|
||||
ATTR_MEDIA_ENQUEUE: cv.boolean,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
@@ -228,6 +233,12 @@ def media_pause(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data)
|
||||
|
||||
|
||||
def media_stop(hass, entity_id=None):
|
||||
"""Send the media player the stop command."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_MEDIA_STOP, data)
|
||||
|
||||
|
||||
def media_next_track(hass, entity_id=None):
|
||||
"""Send the media player the command for next track."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
@@ -247,7 +258,7 @@ def media_seek(hass, position, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data)
|
||||
|
||||
|
||||
def play_media(hass, media_type, media_id, entity_id=None):
|
||||
def play_media(hass, media_type, media_id, entity_id=None, enqueue=None):
|
||||
"""Send the media player the command for playing media."""
|
||||
data = {ATTR_MEDIA_CONTENT_TYPE: media_type,
|
||||
ATTR_MEDIA_CONTENT_ID: media_id}
|
||||
@@ -255,6 +266,9 @@ def play_media(hass, media_type, media_id, entity_id=None):
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
if enqueue:
|
||||
data[ATTR_MEDIA_ENQUEUE] = enqueue
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data)
|
||||
|
||||
|
||||
@@ -355,9 +369,14 @@ def setup(hass, config):
|
||||
"""Play specified media_id on the media player."""
|
||||
media_type = service.data.get(ATTR_MEDIA_CONTENT_TYPE)
|
||||
media_id = service.data.get(ATTR_MEDIA_CONTENT_ID)
|
||||
enqueue = service.data.get(ATTR_MEDIA_ENQUEUE)
|
||||
|
||||
kwargs = {
|
||||
ATTR_MEDIA_ENQUEUE: enqueue,
|
||||
}
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.play_media(media_type, media_id)
|
||||
player.play_media(media_type, media_id, **kwargs)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
@@ -510,6 +529,10 @@ class MediaPlayerDevice(Entity):
|
||||
"""Send pause command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def media_stop(self):
|
||||
"""Send stop command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
raise NotImplementedError()
|
||||
@@ -536,6 +559,11 @@ class MediaPlayerDevice(Entity):
|
||||
"""Boolean if pause is supported."""
|
||||
return bool(self.supported_media_commands & SUPPORT_PAUSE)
|
||||
|
||||
@property
|
||||
def support_stop(self):
|
||||
"""Boolean if stop is supported."""
|
||||
return bool(self.supported_media_commands & SUPPORT_STOP)
|
||||
|
||||
@property
|
||||
def support_seek(self):
|
||||
"""Boolean if seek is supported."""
|
||||
|
||||
@@ -253,7 +253,7 @@ class CastDevice(MediaPlayerDevice):
|
||||
"""Seek the media to a specific location."""
|
||||
self.cast.media_controller.seek(position)
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play media from a URL."""
|
||||
self.cast.media_controller.play_media(media_id, media_type)
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
|
||||
"""Flag of media commands that are supported."""
|
||||
return YOUTUBE_PLAYER_SUPPORT
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play a piece of media."""
|
||||
self.youtube_id = media_id
|
||||
self.update_ha_state()
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
Support for Google Play Music Desktop Player.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.gpmdp/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import socket
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_PAUSE, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['websocket-client==0.35.0']
|
||||
SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the GPMDP platform."""
|
||||
from websocket import create_connection
|
||||
|
||||
name = config.get("name", "GPM Desktop Player")
|
||||
address = config.get("address")
|
||||
|
||||
if address is None:
|
||||
_LOGGER.error("Missing address in config")
|
||||
return False
|
||||
|
||||
add_devices([GPMDP(name, address, create_connection)])
|
||||
|
||||
|
||||
class GPMDP(MediaPlayerDevice):
|
||||
"""Representation of a GPMDP."""
|
||||
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, name, address, create_connection):
|
||||
"""Initialize the media player."""
|
||||
self._connection = create_connection
|
||||
self._address = address
|
||||
self._name = name
|
||||
self._status = STATE_OFF
|
||||
self._ws = None
|
||||
self._title = None
|
||||
self._artist = None
|
||||
self._albumart = None
|
||||
self.update()
|
||||
|
||||
def get_ws(self):
|
||||
"""Check if the websocket is setup and connected."""
|
||||
if self._ws is None:
|
||||
try:
|
||||
self._ws = self._connection(("ws://" + self._address +
|
||||
":5672"), timeout=1)
|
||||
except (socket.timeout, ConnectionRefusedError,
|
||||
ConnectionResetError):
|
||||
self._ws = None
|
||||
elif self._ws.connected is True:
|
||||
self._ws.close()
|
||||
try:
|
||||
self._ws = self._connection(("ws://" + self._address +
|
||||
":5672"), timeout=1)
|
||||
except (socket.timeout, ConnectionRefusedError,
|
||||
ConnectionResetError):
|
||||
self._ws = None
|
||||
return self._ws
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details from the player."""
|
||||
websocket = self.get_ws()
|
||||
if websocket is None:
|
||||
self._status = STATE_OFF
|
||||
return
|
||||
else:
|
||||
state = websocket.recv()
|
||||
state = ((json.loads(state))['payload'])
|
||||
if state is True:
|
||||
websocket.recv()
|
||||
websocket.recv()
|
||||
song = websocket.recv()
|
||||
song = json.loads(song)
|
||||
self._title = (song['payload']['title'])
|
||||
self._artist = (song['payload']['artist'])
|
||||
self._albumart = (song['payload']['albumArt'])
|
||||
self._status = STATE_PLAYING
|
||||
elif state is False:
|
||||
self._status = STATE_PAUSED
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media (Music track only)."""
|
||||
return self._artist
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
return self._albumart
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_GPMDP
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send media_next command to media player."""
|
||||
websocket = self.get_ws()
|
||||
if websocket is None:
|
||||
return
|
||||
websocket.send('{"namespace": "playback", "method": "forward"}')
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send media_previous command to media player."""
|
||||
websocket = self.get_ws()
|
||||
if websocket is None:
|
||||
return
|
||||
websocket.send('{"namespace": "playback", "method": "rewind"}')
|
||||
|
||||
def media_play(self):
|
||||
"""Send media_play command to media player."""
|
||||
websocket = self.get_ws()
|
||||
if websocket is None:
|
||||
return
|
||||
websocket.send('{"namespace": "playback", "method": "playPause"}')
|
||||
self._status = STATE_PAUSED
|
||||
self.update_ha_state()
|
||||
|
||||
def media_pause(self):
|
||||
"""Send media_pause command to media player."""
|
||||
websocket = self.get_ws()
|
||||
if websocket is None:
|
||||
return
|
||||
websocket.send('{"namespace": "playback", "method": "playPause"}')
|
||||
self._status = STATE_PAUSED
|
||||
self.update_ha_state()
|
||||
@@ -320,7 +320,7 @@ class ItunesDevice(MediaPlayerDevice):
|
||||
response = self.client.previous()
|
||||
self.update_state(response)
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Send the play_media command to the media player."""
|
||||
if media_type == MEDIA_TYPE_PLAYLIST:
|
||||
response = self.client.play_playlist(media_id)
|
||||
|
||||
@@ -9,17 +9,17 @@ import urllib
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['jsonrpc-requests==0.1']
|
||||
REQUIREMENTS = ['jsonrpc-requests==0.2']
|
||||
|
||||
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
SUPPORT_PLAY_MEDIA
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_STOP
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -229,6 +229,13 @@ class KodiDevice(MediaPlayerDevice):
|
||||
"""Pause the media player."""
|
||||
self._set_play_state(False)
|
||||
|
||||
def media_stop(self):
|
||||
"""Stop the media player."""
|
||||
players = self._get_players()
|
||||
|
||||
if len(players) != 0:
|
||||
self._server.Player.Stop(players[0]['playerid'])
|
||||
|
||||
def _goto(self, direction):
|
||||
"""Helper method used for previous/next track."""
|
||||
players = self._get_players()
|
||||
@@ -271,6 +278,6 @@ class KodiDevice(MediaPlayerDevice):
|
||||
|
||||
self.update_ha_state()
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Send the play_media command to the media player."""
|
||||
self._server.Player.Open({media_type: media_id}, {})
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Support for LG TV running on NetCast 3 or 4.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.lg_netcast/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from requests import RequestException
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||
SUPPORT_SELECT_SOURCE, MEDIA_TYPE_CHANNEL, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_PLATFORM, CONF_HOST, CONF_NAME, CONF_ACCESS_TOKEN,
|
||||
STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN)
|
||||
import homeassistant.util as util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['https://github.com/wokar/pylgnetcast/archive/'
|
||||
'v0.2.0.zip#pylgnetcast==0.2.0']
|
||||
|
||||
SUPPORT_LGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
|
||||
DEFAULT_NAME = 'LG TV Remote'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "lg_netcast",
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_ACCESS_TOKEN): vol.All(cv.string, vol.Length(max=6)),
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the LG TV platform."""
|
||||
from pylgnetcast import LgNetCastClient
|
||||
client = LgNetCastClient(config[CONF_HOST], config[CONF_ACCESS_TOKEN])
|
||||
add_devices([LgTVDevice(client, config[CONF_NAME])])
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class LgTVDevice(MediaPlayerDevice):
|
||||
"""Representation of a LG TV."""
|
||||
|
||||
def __init__(self, client, name):
|
||||
"""Initialize the LG TV device."""
|
||||
self._client = client
|
||||
self._name = name
|
||||
self._muted = False
|
||||
# Assume that the TV is in Play mode
|
||||
self._playing = True
|
||||
self._volume = 0
|
||||
self._channel_name = ''
|
||||
self._program_name = ''
|
||||
self._state = STATE_UNKNOWN
|
||||
self._sources = {}
|
||||
self._source_names = []
|
||||
|
||||
self.update()
|
||||
|
||||
def send_command(self, command):
|
||||
"""Send remote control commands to the TV."""
|
||||
from pylgnetcast import LgNetCastError
|
||||
try:
|
||||
with self._client as client:
|
||||
client.send_command(command)
|
||||
except (LgNetCastError, RequestException):
|
||||
self._state = STATE_OFF
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update(self):
|
||||
"""Retrieve the latest data from the LG TV."""
|
||||
from pylgnetcast import LgNetCastError
|
||||
try:
|
||||
with self._client as client:
|
||||
self._state = STATE_PLAYING
|
||||
volume_info = client.query_data('volume_info')
|
||||
if volume_info:
|
||||
volume_info = volume_info[0]
|
||||
self._volume = float(volume_info.find('level').text)
|
||||
self._muted = volume_info.find('mute').text == 'true'
|
||||
|
||||
channel_info = client.query_data('cur_channel')
|
||||
if channel_info:
|
||||
channel_info = channel_info[0]
|
||||
self._channel_name = channel_info.find('chname').text
|
||||
self._program_name = channel_info.find('progName').text
|
||||
|
||||
channel_list = client.query_data('channel_list')
|
||||
if channel_list:
|
||||
channel_names = [str(c.find('chname').text) for
|
||||
c in channel_list]
|
||||
self._sources = dict(zip(channel_names, channel_list))
|
||||
# sort source names by the major channel number
|
||||
source_tuples = [(k, self._sources[k].find('major').text)
|
||||
for k in self._sources.keys()]
|
||||
sorted_sources = sorted(
|
||||
source_tuples, key=lambda channel: int(channel[1]))
|
||||
self._source_names = [n for n, k in sorted_sources]
|
||||
except (LgNetCastError, RequestException):
|
||||
self._state = STATE_OFF
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Boolean if volume is currently muted."""
|
||||
return self._muted
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
return self._volume / 100.0
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
return self._channel_name
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return self._source_names
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
return MEDIA_TYPE_CHANNEL
|
||||
|
||||
@property
|
||||
def media_channel(self):
|
||||
"""Channel currently playing."""
|
||||
return self._channel_name
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self._program_name
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_LGTV
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self.send_command(1)
|
||||
|
||||
def volume_up(self):
|
||||
"""Volume up the media player."""
|
||||
self.send_command(24)
|
||||
|
||||
def volume_down(self):
|
||||
"""Volume down media player."""
|
||||
self.send_command(25)
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Send mute command."""
|
||||
self.send_command(26)
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
self._client.change_channel(self._sources[source])
|
||||
|
||||
def media_play_pause(self):
|
||||
"""Simulate play pause media player."""
|
||||
if self._playing:
|
||||
self.media_pause()
|
||||
else:
|
||||
self.media_play()
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
self._playing = True
|
||||
self._state = STATE_PLAYING
|
||||
self.send_command(33)
|
||||
|
||||
def media_pause(self):
|
||||
"""Send media pause command to media player."""
|
||||
self._playing = False
|
||||
self._state = STATE_PAUSED
|
||||
self.send_command(34)
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.send_command(36)
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send the previous track command."""
|
||||
self.send_command(37)
|
||||
@@ -89,7 +89,13 @@ class MpdDevice(MediaPlayerDevice):
|
||||
try:
|
||||
self.status = self.client.status()
|
||||
self.currentsong = self.client.currentsong()
|
||||
except mpd.ConnectionError:
|
||||
except (mpd.ConnectionError, BrokenPipeError, ValueError):
|
||||
# Cleanly disconnect in case connection is not in valid state
|
||||
try:
|
||||
self.client.disconnect()
|
||||
except mpd.ConnectionError:
|
||||
pass
|
||||
|
||||
self.client.connect(self.server, self.port)
|
||||
|
||||
if self.password is not None:
|
||||
@@ -206,7 +212,7 @@ class MpdDevice(MediaPlayerDevice):
|
||||
"""Service to send the MPD the command for previous track."""
|
||||
self.client.previous()
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Send the media player the command for playing a playlist."""
|
||||
_LOGGER.info(str.format("Playing playlist: {0}", media_id))
|
||||
if media_type == MEDIA_TYPE_PLAYLIST:
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME
|
||||
|
||||
REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/'
|
||||
'python3.zip#onkyo-eiscp==0.9.2']
|
||||
@@ -17,29 +17,59 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
KNOWN_HOSTS = []
|
||||
DEFAULT_SOURCES = {"tv": "TV", "bd": "Bluray", "game": "Game", "aux1": "Aux1",
|
||||
"video1": "Video 1", "video2": "Video 2",
|
||||
"video3": "Video 3", "video4": "Video 4",
|
||||
"video5": "Video 5", "video6": "Video 6",
|
||||
"video7": "Video 7"}
|
||||
CONFIG_SOURCE_LIST = "sources"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Onkyo platform."""
|
||||
import eiscp
|
||||
from eiscp import eISCP
|
||||
add_devices(OnkyoDevice(receiver)
|
||||
for receiver in eISCP.discover())
|
||||
hosts = []
|
||||
|
||||
if CONF_HOST in config and config[CONF_HOST] not in KNOWN_HOSTS:
|
||||
try:
|
||||
hosts.append(OnkyoDevice(eiscp.eISCP(config[CONF_HOST]),
|
||||
config.get(CONFIG_SOURCE_LIST,
|
||||
DEFAULT_SOURCES),
|
||||
name=config[CONF_NAME]))
|
||||
KNOWN_HOSTS.append(config[CONF_HOST])
|
||||
except OSError:
|
||||
_LOGGER.error('Unable to connect to receiver at %s.',
|
||||
config[CONF_HOST])
|
||||
else:
|
||||
for receiver in eISCP.discover():
|
||||
if receiver.host not in KNOWN_HOSTS:
|
||||
hosts.append(OnkyoDevice(receiver,
|
||||
config.get(CONFIG_SOURCE_LIST,
|
||||
DEFAULT_SOURCES)))
|
||||
KNOWN_HOSTS.append(receiver.host)
|
||||
add_devices(hosts)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class OnkyoDevice(MediaPlayerDevice):
|
||||
"""Representation of a Onkyo device."""
|
||||
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
def __init__(self, receiver):
|
||||
def __init__(self, receiver, sources, name=None):
|
||||
"""Initialize the Onkyo Receiver."""
|
||||
self._receiver = receiver
|
||||
self._muted = False
|
||||
self._volume = 0
|
||||
self._pwstate = STATE_OFF
|
||||
self.update()
|
||||
self._name = '{}_{}'.format(
|
||||
self._name = name or '{}_{}'.format(
|
||||
receiver.info['model_name'], receiver.info['identifier'])
|
||||
self._current_source = None
|
||||
self._source_list = list(sources.values())
|
||||
self._source_mapping = sources
|
||||
self._reverse_mapping = {value: key for key, value in sources.items()}
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details from the device."""
|
||||
@@ -52,8 +82,13 @@ class OnkyoDevice(MediaPlayerDevice):
|
||||
volume_raw = self._receiver.command('volume query')
|
||||
mute_raw = self._receiver.command('audio-muting query')
|
||||
current_source_raw = self._receiver.command('input-selector query')
|
||||
self._current_source = '_'.join('_'.join(
|
||||
[i for i in current_source_raw[1]]))
|
||||
for source in current_source_raw[1]:
|
||||
if source in self._source_mapping:
|
||||
self._current_source = self._source_mapping[source]
|
||||
break
|
||||
else:
|
||||
self._current_source = '_'.join(
|
||||
[i for i in current_source_raw[1]])
|
||||
self._muted = bool(mute_raw[1] == 'on')
|
||||
self._volume = int(volume_raw[1], 16)/80.0
|
||||
|
||||
@@ -87,6 +122,11 @@ class OnkyoDevice(MediaPlayerDevice):
|
||||
""""Return the current input source of the device."""
|
||||
return self._current_source
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return self._source_list
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self._receiver.command('system-power standby')
|
||||
@@ -108,4 +148,6 @@ class OnkyoDevice(MediaPlayerDevice):
|
||||
|
||||
def select_source(self, source):
|
||||
"""Set the input source."""
|
||||
if source in self._source_list:
|
||||
source = self._reverse_mapping[source]
|
||||
self._receiver.command('input-selector {}'.format(source))
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Support for Pioneer Network Receivers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.pioneer/
|
||||
"""
|
||||
import logging
|
||||
import telnetlib
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
DOMAIN, SUPPORT_PAUSE, SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN,
|
||||
CONF_NAME)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_PIONEER = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
|
||||
MAX_VOLUME = 185
|
||||
MAX_SOURCE_NUMBERS = 60
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Pioneer platform."""
|
||||
if not config.get(CONF_HOST):
|
||||
_LOGGER.error(
|
||||
"Missing required configuration items in %s: %s",
|
||||
DOMAIN,
|
||||
CONF_HOST)
|
||||
return False
|
||||
|
||||
pioneer = PioneerDevice(
|
||||
config.get(CONF_NAME, "Pioneer AVR"),
|
||||
config.get(CONF_HOST)
|
||||
)
|
||||
if pioneer.update():
|
||||
add_devices([pioneer])
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class PioneerDevice(MediaPlayerDevice):
|
||||
"""Representation of a Pioneer device."""
|
||||
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, name, host):
|
||||
"""Initialize the Pioneer device."""
|
||||
self._name = name
|
||||
self._host = host
|
||||
self._pwstate = "PWR1"
|
||||
self._volume = 0
|
||||
self._muted = False
|
||||
self._selected_source = ''
|
||||
self._source_name_to_number = {}
|
||||
self._source_number_to_name = {}
|
||||
|
||||
@classmethod
|
||||
def telnet_request(cls, telnet, command, expected_prefix):
|
||||
"""Execute `command` and return the response."""
|
||||
telnet.write(command.encode("ASCII") + b"\r")
|
||||
|
||||
# The receiver will randomly send state change updates, make sure
|
||||
# we get the response we are looking for
|
||||
for _ in range(3):
|
||||
result = telnet.read_until(b"\r\n", timeout=0.2).decode("ASCII") \
|
||||
.strip()
|
||||
if result.startswith(expected_prefix):
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
def telnet_command(self, command):
|
||||
"""Establish a telnet connection and sends `command`."""
|
||||
telnet = telnetlib.Telnet(self._host)
|
||||
telnet.write(command.encode("ASCII") + b"\r")
|
||||
telnet.read_very_eager() # skip response
|
||||
telnet.close()
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details from the device."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self._host)
|
||||
except ConnectionRefusedError:
|
||||
return False
|
||||
|
||||
self._pwstate = self.telnet_request(telnet, "?P", "PWR")
|
||||
|
||||
volume_str = self.telnet_request(telnet, "?V", "VOL")
|
||||
self._volume = int(volume_str[3:]) / MAX_VOLUME if volume_str else None
|
||||
|
||||
muted_value = self.telnet_request(telnet, "?M", "MUT")
|
||||
self._muted = (muted_value == "MUT0") if muted_value else None
|
||||
|
||||
# Build the source name dictionaries if necessary
|
||||
if not self._source_name_to_number:
|
||||
for i in range(MAX_SOURCE_NUMBERS):
|
||||
result = self.telnet_request(telnet,
|
||||
"?RGB" + str(i).zfill(2),
|
||||
"RGB")
|
||||
|
||||
if not result:
|
||||
continue
|
||||
|
||||
source_name = result[6:]
|
||||
source_number = str(i).zfill(2)
|
||||
|
||||
self._source_name_to_number[source_name] = source_number
|
||||
self._source_number_to_name[source_number] = source_name
|
||||
|
||||
source_number = self.telnet_request(telnet, "?F", "FN")
|
||||
|
||||
if source_number:
|
||||
self._selected_source = self._source_number_to_name \
|
||||
.get(source_number[2:])
|
||||
else:
|
||||
self._selected_source = None
|
||||
|
||||
telnet.close()
|
||||
return True
|
||||
|
||||
@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._pwstate == "PWR1":
|
||||
return STATE_OFF
|
||||
if self._pwstate == "PWR0":
|
||||
return STATE_ON
|
||||
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
return self._volume
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Boolean if volume is currently muted."""
|
||||
return self._muted
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_PIONEER
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
return self._selected_source
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return list(self._source_name_to_number.keys())
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self._selected_source
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self.telnet_command("PF")
|
||||
|
||||
def volume_up(self):
|
||||
"""Volume up media player."""
|
||||
self.telnet_command("VU")
|
||||
|
||||
def volume_down(self):
|
||||
"""Volume down media player."""
|
||||
self.telnet_command("VD")
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
# 60dB max
|
||||
self.telnet_command(str(round(volume * MAX_VOLUME)).zfill(3) + "VL")
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
self.telnet_command("MO" if mute else "MF")
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
self.telnet_command("PO")
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
self.telnet_command(self._source_name_to_number.get(source) + "FN")
|
||||
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
Support for the roku media player.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.roku/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME)
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/bah2830/python-roku/archive/3.1.1.zip'
|
||||
'#python-roku==3.1.1']
|
||||
|
||||
KNOWN_HOSTS = []
|
||||
DEFAULT_PORT = 8060
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
||||
SUPPORT_SELECT_SOURCE
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Roku platform."""
|
||||
hosts = []
|
||||
|
||||
if discovery_info and discovery_info in KNOWN_HOSTS:
|
||||
return
|
||||
|
||||
if discovery_info is not None:
|
||||
_LOGGER.debug('Discovered Roku: %s', discovery_info[0])
|
||||
hosts.append(discovery_info[0])
|
||||
|
||||
elif CONF_HOST in config:
|
||||
hosts.append(config[CONF_HOST])
|
||||
|
||||
rokus = []
|
||||
for host in hosts:
|
||||
rokus.append(RokuDevice(host))
|
||||
KNOWN_HOSTS.append(host)
|
||||
|
||||
add_devices(rokus)
|
||||
|
||||
|
||||
class RokuDevice(MediaPlayerDevice):
|
||||
"""Representation of a Roku device on the network."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-public-methods
|
||||
def __init__(self, host):
|
||||
"""Initialize the Roku device."""
|
||||
from roku import Roku
|
||||
|
||||
self.roku = Roku(host)
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self.roku_name = "roku_" + self.roku.device_info.sernum
|
||||
self.ip_address = self.roku.host
|
||||
self.channels = self.get_source_list()
|
||||
|
||||
if self.roku.current_app is not None:
|
||||
self.current_app = self.roku.current_app
|
||||
else:
|
||||
self.current_app = None
|
||||
|
||||
def get_source_list(self):
|
||||
"""Get the list of applications to be used as sources."""
|
||||
return ["Home"] + sorted(channel.name for channel in self.roku.apps)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Device should be polled."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self.roku_name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self.current_app.name in ["Power Saver", "Default screensaver"]:
|
||||
return STATE_IDLE
|
||||
elif self.current_app.name == "Roku":
|
||||
return STATE_HOME
|
||||
elif self.current_app.name is not None:
|
||||
return STATE_PLAYING
|
||||
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_ROKU
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
if self.current_app is None:
|
||||
return None
|
||||
elif self.current_app.name == "Power Saver":
|
||||
return None
|
||||
elif self.current_app.name == "Roku":
|
||||
return None
|
||||
else:
|
||||
return MEDIA_TYPE_VIDEO
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
if self.current_app is None:
|
||||
return None
|
||||
elif self.current_app.name == "Roku":
|
||||
return None
|
||||
elif self.current_app.name == "Power Saver":
|
||||
return None
|
||||
elif self.current_app.id is None:
|
||||
return None
|
||||
|
||||
return 'http://{0}:{1}/query/icon/{2}'.format(self.ip_address,
|
||||
DEFAULT_PORT,
|
||||
self.current_app.id)
|
||||
|
||||
@property
|
||||
def app_name(self):
|
||||
"""Name of the current running app."""
|
||||
return self.current_app.name
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
"""Return the ID of the current running app."""
|
||||
return self.current_app.id
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
return self.current_app.name
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return self.channels
|
||||
|
||||
def media_play_pause(self):
|
||||
"""Send play/pause command."""
|
||||
self.roku.play()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
self.roku.reverse()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.roku.forward()
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Mute the volume."""
|
||||
self.roku.volume_mute()
|
||||
|
||||
def volume_up(self):
|
||||
"""Volume up media player."""
|
||||
self.roku.volume_up()
|
||||
|
||||
def volume_down(self):
|
||||
"""Volume down media player."""
|
||||
self.roku.volume_down()
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
if source == "Home":
|
||||
self.roku.home()
|
||||
else:
|
||||
channel = self.roku[source]
|
||||
channel.launch()
|
||||
@@ -86,6 +86,14 @@ media_pause:
|
||||
description: Name(s) of entities to pause on
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
media_stop:
|
||||
description: Send the media player the stop command.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to stop on
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
media_next_track:
|
||||
description: Send the media player the command for next track.
|
||||
|
||||
@@ -137,3 +145,11 @@ select_source:
|
||||
source:
|
||||
description: Name of the source to switch to. Platform dependent.
|
||||
example: 'video1'
|
||||
|
||||
sonos_group_players:
|
||||
description: Send Sonos media player the command for grouping all players into one (party mode).
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
@@ -9,12 +9,16 @@ import logging
|
||||
import socket
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN)
|
||||
|
||||
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_SELECT_SOURCE
|
||||
|
||||
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
||||
DOMAIN = 'snapcast'
|
||||
REQUIREMENTS = ['snapcast==1.1.1']
|
||||
REQUIREMENTS = ['snapcast==1.2.1']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -67,9 +71,23 @@ class SnapcastDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the player."""
|
||||
if self._client.connected:
|
||||
return STATE_ON
|
||||
return STATE_OFF
|
||||
if not self._client.connected:
|
||||
return STATE_OFF
|
||||
return {
|
||||
'idle': STATE_IDLE,
|
||||
'playing': STATE_PLAYING,
|
||||
'unkown': STATE_UNKNOWN,
|
||||
}.get(self._client.stream.status, STATE_UNKNOWN)
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
return self._client.stream.identifier
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return self._client.available_streams()
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Send the mute command."""
|
||||
@@ -78,3 +96,7 @@ class SnapcastDevice(MediaPlayerDevice):
|
||||
def set_volume_level(self, volume):
|
||||
"""Set the volume level."""
|
||||
self._client.volume = round(volume * 100)
|
||||
|
||||
def select_source(self, source):
|
||||
"""Set input source."""
|
||||
self._client.stream = source
|
||||
|
||||
@@ -7,14 +7,15 @@ https://home-assistant.io/components/media_player.sonos/
|
||||
import datetime
|
||||
import logging
|
||||
import socket
|
||||
from os import path
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
MediaPlayerDevice)
|
||||
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
REQUIREMENTS = ['SoCo==0.11.1']
|
||||
|
||||
@@ -32,6 +33,8 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
|
||||
SUPPORT_SEEK
|
||||
|
||||
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -63,9 +66,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.warning('No Sonos speakers found.')
|
||||
return False
|
||||
|
||||
add_devices(SonosDevice(hass, p) for p in players)
|
||||
devices = [SonosDevice(hass, p) for p in players]
|
||||
add_devices(devices)
|
||||
_LOGGER.info('Added %s Sonos speakers', len(players))
|
||||
|
||||
def group_players_service(service):
|
||||
"""Group media players, use player as coordinator."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
device.group_players()
|
||||
device.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS,
|
||||
group_players_service,
|
||||
descriptions.get(SERVICE_GROUP_PLAYERS))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -74,16 +99,26 @@ def only_if_coordinator(func):
|
||||
|
||||
If used as decorator, avoid calling the decorated method if player is not
|
||||
a coordinator. If not, a grouped speaker (not in coordinator role) will
|
||||
throw soco.exceptions.SoCoSlaveException
|
||||
throw soco.exceptions.SoCoSlaveException.
|
||||
|
||||
Also, partially catch exceptions like:
|
||||
|
||||
soco.exceptions.SoCoUPnPException: UPnP Error 701 received:
|
||||
Transition not available from <player ip address>
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Decorator wrapper."""
|
||||
if args[0].is_coordinator:
|
||||
return func(*args, **kwargs)
|
||||
from soco.exceptions import SoCoUPnPException
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
except SoCoUPnPException:
|
||||
_LOGGER.error('command "%s" for Sonos device "%s" '
|
||||
'not available in this mode',
|
||||
func.__name__, args[0].name)
|
||||
else:
|
||||
_LOGGER.debug('Ignore command "%s" for Sonos device "%s" '
|
||||
'(not coordinator)',
|
||||
func.__name__, args[0].name)
|
||||
_LOGGER.debug('Ignore command "%s" for Sonos device "%s" (%s)',
|
||||
func.__name__, args[0].name, 'not coordinator')
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -104,7 +139,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
"""Polling needed."""
|
||||
return True
|
||||
|
||||
def update_sonos(self, now):
|
||||
@@ -258,9 +293,27 @@ class SonosDevice(MediaPlayerDevice):
|
||||
self._player.play()
|
||||
|
||||
@only_if_coordinator
|
||||
def play_media(self, media_type, media_id):
|
||||
"""Send the play_media command to the media player."""
|
||||
self._player.play_uri(media_id)
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""
|
||||
Send the play_media command to the media player.
|
||||
|
||||
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
|
||||
"""
|
||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||
from soco.exceptions import SoCoUPnPException
|
||||
try:
|
||||
self._player.add_uri_to_queue(media_id)
|
||||
except SoCoUPnPException:
|
||||
_LOGGER.error('Error parsing media uri "%s", '
|
||||
"please check it's a valid media resource "
|
||||
'supported by Sonos', media_id)
|
||||
else:
|
||||
self._player.play_uri(media_id)
|
||||
|
||||
@only_if_coordinator
|
||||
def group_players(self):
|
||||
"""Group all players under this coordinator."""
|
||||
self._player.partymode()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
|
||||
@@ -25,7 +25,8 @@ from homeassistant.const import (
|
||||
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_ON)
|
||||
SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_ON,
|
||||
SERVICE_MEDIA_STOP)
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
|
||||
@@ -384,6 +385,10 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
||||
"""Send pause command."""
|
||||
self._call_service(SERVICE_MEDIA_PAUSE)
|
||||
|
||||
def media_stop(self):
|
||||
"""Send stop command."""
|
||||
self._call_service(SERVICE_MEDIA_STOP)
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK)
|
||||
@@ -397,7 +402,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
||||
data = {ATTR_MEDIA_SEEK_POSITION: position}
|
||||
self._call_service(SERVICE_MEDIA_SEEK, data)
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play a piece of media."""
|
||||
data = {ATTR_MEDIA_CONTENT_TYPE: media_type,
|
||||
ATTR_MEDIA_CONTENT_ID: media_id}
|
||||
|
||||
@@ -69,7 +69,7 @@ def setup_tv(host, hass, add_devices):
|
||||
_LOGGER.warning(
|
||||
'Connected to LG WebOS TV at %s but not paired.', host)
|
||||
return
|
||||
except ConnectionRefusedError:
|
||||
except OSError:
|
||||
_LOGGER.error('Unable to connect to host %s.', host)
|
||||
return
|
||||
else:
|
||||
@@ -158,7 +158,7 @@ class LgWebOSDevice(MediaPlayerDevice):
|
||||
if source['appId'] == self._current_source_id:
|
||||
self._current_source = source['label']
|
||||
|
||||
except ConnectionRefusedError:
|
||||
except OSError:
|
||||
self._state = STATE_OFF
|
||||
|
||||
@property
|
||||
@@ -208,6 +208,7 @@ class LgWebOSDevice(MediaPlayerDevice):
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self._state = STATE_OFF
|
||||
self._client.power_off()
|
||||
|
||||
def volume_up(self):
|
||||
|
||||
@@ -16,21 +16,30 @@ REQUIREMENTS = ['rxv==0.1.11']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
|
||||
CONF_SOURCE_NAMES = 'source_names'
|
||||
CONF_SOURCE_IGNORE = 'source_ignore'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Yamaha platform."""
|
||||
import rxv
|
||||
add_devices(YamahaDevice(config.get("name"), receiver)
|
||||
for receiver in rxv.find())
|
||||
|
||||
source_ignore = config.get(CONF_SOURCE_IGNORE, [])
|
||||
source_names = config.get(CONF_SOURCE_NAMES, {})
|
||||
|
||||
add_devices(
|
||||
YamahaDevice(config.get("name"), receiver, source_ignore, source_names)
|
||||
for receiver in rxv.find())
|
||||
|
||||
|
||||
class YamahaDevice(MediaPlayerDevice):
|
||||
"""Representation of a Yamaha device."""
|
||||
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
def __init__(self, name, receiver):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, name, receiver, source_ignore, source_names):
|
||||
"""Initialize the Yamaha Receiver."""
|
||||
self._receiver = receiver
|
||||
self._muted = False
|
||||
@@ -38,6 +47,9 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
self._pwstate = STATE_OFF
|
||||
self._current_source = None
|
||||
self._source_list = None
|
||||
self._source_ignore = source_ignore
|
||||
self._source_names = source_names
|
||||
self._reverse_mapping = None
|
||||
self.update()
|
||||
self._name = name
|
||||
|
||||
@@ -48,9 +60,24 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
else:
|
||||
self._pwstate = STATE_OFF
|
||||
self._muted = self._receiver.mute
|
||||
self._volume = (self._receiver.volume/100) + 1
|
||||
self._current_source = self._receiver.input
|
||||
self._source_list = list(self._receiver.inputs().keys())
|
||||
self._volume = (self._receiver.volume / 100) + 1
|
||||
|
||||
if self.source_list is None:
|
||||
self.build_source_list()
|
||||
|
||||
current_source = self._receiver.input
|
||||
self._current_source = self._source_names.get(current_source,
|
||||
current_source)
|
||||
|
||||
def build_source_list(self):
|
||||
"""Build the source list."""
|
||||
self._reverse_mapping = {alias: source for source, alias in
|
||||
self._source_names.items()}
|
||||
|
||||
self._source_list = sorted(
|
||||
self._source_names.get(source, source) for source in
|
||||
self._receiver.inputs()
|
||||
if source not in self._source_ignore)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -93,7 +120,7 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
receiver_vol = 100-(volume * 100)
|
||||
receiver_vol = 100 - (volume * 100)
|
||||
negative_receiver_vol = -receiver_vol
|
||||
self._receiver.volume = negative_receiver_vol
|
||||
|
||||
@@ -104,8 +131,9 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
self._receiver.on = True
|
||||
self._volume = (self._receiver.volume/100) + 1
|
||||
self._volume = (self._receiver.volume / 100) + 1
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
self._receiver.input = source
|
||||
self._receiver.input = self._reverse_mapping.get(source,
|
||||
source)
|
||||
|
||||
@@ -39,6 +39,9 @@ CONF_KEEPALIVE = 'keepalive'
|
||||
CONF_USERNAME = 'username'
|
||||
CONF_PASSWORD = 'password'
|
||||
CONF_CERTIFICATE = 'certificate'
|
||||
CONF_CLIENT_KEY = 'client_key'
|
||||
CONF_CLIENT_CERT = 'client_cert'
|
||||
CONF_TLS_INSECURE = 'tls_insecure'
|
||||
CONF_PROTOCOL = 'protocol'
|
||||
|
||||
CONF_STATE_TOPIC = 'state_topic'
|
||||
@@ -78,6 +81,9 @@ def valid_publish_topic(value):
|
||||
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
|
||||
_HBMQTT_CONFIG_SCHEMA = vol.Schema(dict)
|
||||
|
||||
CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \
|
||||
'the mqtt broker config'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_CLIENT_ID): cv.string,
|
||||
@@ -89,6 +95,11 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CERTIFICATE): cv.isfile,
|
||||
vol.Inclusive(CONF_CLIENT_KEY, 'client_key_auth',
|
||||
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
|
||||
vol.Inclusive(CONF_CLIENT_CERT, 'client_key_auth',
|
||||
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
|
||||
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
|
||||
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
|
||||
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
|
||||
vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA,
|
||||
@@ -192,20 +203,26 @@ def setup(hass, config):
|
||||
|
||||
broker_config = _setup_server(hass, config)
|
||||
|
||||
broker_in_conf = True if CONF_BROKER in conf else False
|
||||
|
||||
# Only auto config if no server config was passed in
|
||||
if broker_config and CONF_EMBEDDED not in conf:
|
||||
broker, port, username, password, certificate, protocol = broker_config
|
||||
elif not broker_config and (CONF_EMBEDDED in conf or
|
||||
CONF_BROKER not in conf):
|
||||
# Embedded broker doesn't have some ssl variables
|
||||
client_key, client_cert, tls_insecure = None, None, None
|
||||
elif not broker_config and CONF_BROKER not in conf:
|
||||
_LOGGER.error('Unable to start broker and auto-configure MQTT.')
|
||||
return False
|
||||
|
||||
if CONF_BROKER in conf:
|
||||
if broker_in_conf:
|
||||
broker = conf[CONF_BROKER]
|
||||
port = conf[CONF_PORT]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
certificate = conf.get(CONF_CERTIFICATE)
|
||||
client_key = conf.get(CONF_CLIENT_KEY)
|
||||
client_cert = conf.get(CONF_CLIENT_CERT)
|
||||
tls_insecure = conf.get(CONF_TLS_INSECURE)
|
||||
protocol = conf[CONF_PROTOCOL]
|
||||
|
||||
# For cloudmqtt.com, secured connection, auto fill in certificate
|
||||
@@ -216,8 +233,9 @@ def setup(hass, config):
|
||||
|
||||
global MQTT_CLIENT
|
||||
try:
|
||||
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
|
||||
password, certificate, protocol)
|
||||
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive,
|
||||
username, password, certificate, client_key,
|
||||
client_cert, tls_insecure, protocol)
|
||||
except socket.error:
|
||||
_LOGGER.exception("Can't connect to the broker. "
|
||||
"Please check your settings and the broker "
|
||||
@@ -268,7 +286,8 @@ class MQTT(object):
|
||||
"""Home Assistant MQTT client."""
|
||||
|
||||
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
||||
password, certificate, protocol):
|
||||
password, certificate, client_key, client_cert,
|
||||
tls_insecure, protocol):
|
||||
"""Initialize Home Assistant MQTT client."""
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
@@ -288,8 +307,13 @@ class MQTT(object):
|
||||
|
||||
if username is not None:
|
||||
self._mqttc.username_pw_set(username, password)
|
||||
|
||||
if certificate is not None:
|
||||
self._mqttc.tls_set(certificate)
|
||||
self._mqttc.tls_set(certificate, certfile=client_cert,
|
||||
keyfile=client_key)
|
||||
|
||||
if tls_insecure is not None:
|
||||
self._mqttc.tls_insecure_set(tls_insecure)
|
||||
|
||||
self._mqttc.on_subscribe = self._mqtt_on_subscribe
|
||||
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
|
||||
@@ -364,6 +388,8 @@ class MQTT(object):
|
||||
|
||||
def _mqtt_on_message(self, _mqttc, _userdata, msg):
|
||||
"""Message received callback."""
|
||||
_LOGGER.debug("received message on %s: %s",
|
||||
msg.topic, msg.payload.decode('utf-8'))
|
||||
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
|
||||
ATTR_TOPIC: msg.topic,
|
||||
ATTR_QOS: msg.qos,
|
||||
|
||||
@@ -12,7 +12,7 @@ import threading
|
||||
from homeassistant.components.mqtt import PROTOCOL_311
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
REQUIREMENTS = ['hbmqtt==0.6.3']
|
||||
REQUIREMENTS = ['hbmqtt==0.7.1']
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ import logging
|
||||
import socket
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
from homeassistant.const import (ATTR_DISCOVERED, ATTR_SERVICE,
|
||||
CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START,
|
||||
from homeassistant.const import (ATTR_BATTERY_LEVEL, ATTR_DISCOVERED,
|
||||
ATTR_SERVICE, CONF_OPTIMISTIC,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_PLATFORM_DISCOVERED, TEMP_CELSIUS)
|
||||
EVENT_PLATFORM_DISCOVERED, STATE_OFF,
|
||||
STATE_ON, TEMP_CELSIUS)
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
CONF_GATEWAYS = 'gateways'
|
||||
@@ -170,6 +172,8 @@ def pf_callback_factory(map_sv_types, devices, add_devices, entity_class):
|
||||
class GatewayWrapper(object):
|
||||
"""Gateway wrapper class."""
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
def __init__(self, gateway, version, optimistic):
|
||||
"""Setup class attributes on instantiation.
|
||||
|
||||
@@ -182,14 +186,12 @@ class GatewayWrapper(object):
|
||||
_wrapped_gateway (mysensors.SerialGateway): Wrapped gateway.
|
||||
version (str): Version of mysensors API.
|
||||
platform_callbacks (list): Callback functions, one per platform.
|
||||
const (module): Mysensors API constants.
|
||||
optimistic (bool): Send values to actuators without feedback state.
|
||||
__initialised (bool): True if GatewayWrapper is initialised.
|
||||
"""
|
||||
self._wrapped_gateway = gateway
|
||||
self.version = version
|
||||
self.platform_callbacks = []
|
||||
self.const = self.get_const()
|
||||
self.optimistic = optimistic
|
||||
self.__initialised = True
|
||||
|
||||
@@ -197,9 +199,9 @@ class GatewayWrapper(object):
|
||||
"""See if this object has attribute name."""
|
||||
# Do not use hasattr, it goes into infinite recurrsion
|
||||
if name in self.__dict__:
|
||||
# this object has it
|
||||
# This object has the attribute.
|
||||
return getattr(self, name)
|
||||
# proxy to the wrapped object
|
||||
# The wrapped object has the attribute.
|
||||
return getattr(self._wrapped_gateway, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
@@ -211,14 +213,6 @@ class GatewayWrapper(object):
|
||||
else:
|
||||
object.__setattr__(self._wrapped_gateway, name, value)
|
||||
|
||||
def get_const(self):
|
||||
"""Get mysensors API constants."""
|
||||
if self.version == '1.5':
|
||||
import mysensors.const_15 as const
|
||||
else:
|
||||
import mysensors.const_14 as const
|
||||
return const
|
||||
|
||||
def callback_factory(self):
|
||||
"""Return a new callback function."""
|
||||
def node_update(update_type, node_id):
|
||||
@@ -228,3 +222,99 @@ class GatewayWrapper(object):
|
||||
callback(self, node_id)
|
||||
|
||||
return node_update
|
||||
|
||||
|
||||
class MySensorsDeviceEntity(object):
|
||||
"""Represent a MySensors entity."""
|
||||
|
||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||
|
||||
def __init__(
|
||||
self, gateway, node_id, child_id, name, value_type, child_type):
|
||||
"""
|
||||
Setup class attributes on instantiation.
|
||||
|
||||
Args:
|
||||
gateway (GatewayWrapper): Gateway object.
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
child_type (str): Child type of child.
|
||||
|
||||
Attributes:
|
||||
gateway (GatewayWrapper): Gateway object.
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
_name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
child_type (str): Child type of child.
|
||||
battery_level (int): Node battery level.
|
||||
_values (dict): Child values. Non state values set as state attributes.
|
||||
mysensors (module): Mysensors main component module.
|
||||
"""
|
||||
self.gateway = gateway
|
||||
self.node_id = node_id
|
||||
self.child_id = child_id
|
||||
self._name = name
|
||||
self.value_type = value_type
|
||||
self.child_type = child_type
|
||||
self.battery_level = 0
|
||||
self._values = {}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Mysensor gateway pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of this entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
address = getattr(self.gateway, 'server_address', None)
|
||||
if address:
|
||||
device = '{}:{}'.format(address[0], address[1])
|
||||
else:
|
||||
device = self.gateway.port
|
||||
attr = {
|
||||
ATTR_DEVICE: device,
|
||||
ATTR_NODE_ID: self.node_id,
|
||||
ATTR_CHILD_ID: self.child_id,
|
||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||
}
|
||||
|
||||
set_req = self.gateway.const.SetReq
|
||||
|
||||
for value_type, value in self._values.items():
|
||||
try:
|
||||
attr[set_req(value_type).name] = value
|
||||
except ValueError:
|
||||
_LOGGER.error('value_type %s is not valid for mysensors '
|
||||
'version %s', value_type,
|
||||
self.gateway.version)
|
||||
return attr
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.value_type in self._values
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
child = node.children[self.child_id]
|
||||
self.battery_level = node.battery_level
|
||||
set_req = self.gateway.const.SetReq
|
||||
for value_type, value in child.values.items():
|
||||
_LOGGER.debug(
|
||||
"%s: value_type %s, value = %s", self._name, value_type, value)
|
||||
if value_type in (set_req.V_ARMED, set_req.V_LIGHT,
|
||||
set_req.V_LOCK_STATUS, set_req.V_TRIPPED):
|
||||
self._values[value_type] = (
|
||||
STATE_ON if int(value) == 1 else STATE_OFF)
|
||||
else:
|
||||
self._values[value_type] = value
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for Nest thermostats.
|
||||
Support for Nest thermostats and protect smoke alarms.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/thermostat.nest/
|
||||
@@ -11,7 +11,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
REQUIREMENTS = ['python-nest==2.6.0']
|
||||
REQUIREMENTS = ['python-nest==2.9.2']
|
||||
DOMAIN = 'nest'
|
||||
|
||||
NEST = None
|
||||
@@ -36,6 +36,16 @@ def devices():
|
||||
_LOGGER.error("Connection error logging into the nest web service.")
|
||||
|
||||
|
||||
def protect_devices():
|
||||
"""Generator returning list of protect devices."""
|
||||
try:
|
||||
for structure in NEST.structures:
|
||||
for device in structure.protectdevices:
|
||||
yield(structure, device)
|
||||
except socket.error:
|
||||
_LOGGER.error("Connection error logging into the nest web service.")
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Setup the Nest thermostat component."""
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
AWS Lambda platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.aws_lambda/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import base64
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_PLATFORM, CONF_NAME)
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, BaseNotificationService)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ["boto3==1.3.1"]
|
||||
|
||||
CONF_REGION = "region_name"
|
||||
CONF_ACCESS_KEY_ID = "aws_access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
|
||||
CONF_PROFILE_NAME = "profile_name"
|
||||
CONF_CONTEXT = "context"
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "aws_lambda",
|
||||
vol.Optional(CONF_NAME): vol.Coerce(str),
|
||||
vol.Optional(CONF_REGION, default="us-east-1"): vol.Coerce(str),
|
||||
vol.Inclusive(CONF_ACCESS_KEY_ID, "credentials"): vol.Coerce(str),
|
||||
vol.Inclusive(CONF_SECRET_ACCESS_KEY, "credentials"): vol.Coerce(str),
|
||||
vol.Exclusive(CONF_PROFILE_NAME, "credentials"): vol.Coerce(str),
|
||||
vol.Optional(CONF_CONTEXT, default=dict()): vol.Coerce(dict)
|
||||
})
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the AWS Lambda notification service."""
|
||||
context_str = json.dumps({'hass': hass.config.as_dict(),
|
||||
'custom': config[CONF_CONTEXT]})
|
||||
context_b64 = base64.b64encode(context_str.encode("utf-8"))
|
||||
context = context_b64.decode("utf-8")
|
||||
|
||||
# pylint: disable=import-error
|
||||
import boto3
|
||||
|
||||
aws_config = config.copy()
|
||||
|
||||
del aws_config[CONF_PLATFORM]
|
||||
del aws_config[CONF_NAME]
|
||||
del aws_config[CONF_CONTEXT]
|
||||
|
||||
profile = aws_config.get(CONF_PROFILE_NAME)
|
||||
|
||||
if profile is not None:
|
||||
boto3.setup_default_session(profile_name=profile)
|
||||
del aws_config[CONF_PROFILE_NAME]
|
||||
|
||||
lambda_client = boto3.client("lambda", **aws_config)
|
||||
|
||||
return AWSLambda(lambda_client, context)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class AWSLambda(BaseNotificationService):
|
||||
"""Implement the notification service for the AWS Lambda service."""
|
||||
|
||||
def __init__(self, lambda_client, context):
|
||||
"""Initialize the service."""
|
||||
self.client = lambda_client
|
||||
self.context = context
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send notification to specified LAMBDA ARN."""
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
|
||||
if not targets:
|
||||
_LOGGER.info("At least 1 target is required")
|
||||
return
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
for target in targets:
|
||||
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
|
||||
payload = {"message": message}
|
||||
payload.update(cleaned_kwargs)
|
||||
|
||||
self.client.invoke(FunctionName=target,
|
||||
Payload=json.dumps(payload),
|
||||
ClientContext=self.context)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
AWS SNS platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.aws_sns/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_PLATFORM, CONF_NAME)
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TITLE, ATTR_TARGET, BaseNotificationService)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ["boto3==1.3.1"]
|
||||
|
||||
CONF_REGION = "region_name"
|
||||
CONF_ACCESS_KEY_ID = "aws_access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
|
||||
CONF_PROFILE_NAME = "profile_name"
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "aws_sns",
|
||||
vol.Optional(CONF_NAME): vol.Coerce(str),
|
||||
vol.Optional(CONF_REGION, default="us-east-1"): vol.Coerce(str),
|
||||
vol.Inclusive(CONF_ACCESS_KEY_ID, "credentials"): vol.Coerce(str),
|
||||
vol.Inclusive(CONF_SECRET_ACCESS_KEY, "credentials"): vol.Coerce(str),
|
||||
vol.Exclusive(CONF_PROFILE_NAME, "credentials"): vol.Coerce(str)
|
||||
})
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the AWS SNS notification service."""
|
||||
# pylint: disable=import-error
|
||||
import boto3
|
||||
|
||||
aws_config = config.copy()
|
||||
|
||||
del aws_config[CONF_PLATFORM]
|
||||
del aws_config[CONF_NAME]
|
||||
|
||||
profile = aws_config.get(CONF_PROFILE_NAME)
|
||||
|
||||
if profile is not None:
|
||||
boto3.setup_default_session(profile_name=profile)
|
||||
del aws_config[CONF_PROFILE_NAME]
|
||||
|
||||
sns_client = boto3.client("sns", **aws_config)
|
||||
|
||||
return AWSSNS(sns_client)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class AWSSNS(BaseNotificationService):
|
||||
"""Implement the notification service for the AWS SNS service."""
|
||||
|
||||
def __init__(self, sns_client):
|
||||
"""Initialize the service."""
|
||||
self.client = sns_client
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send notification to specified SNS ARN."""
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
|
||||
if not targets:
|
||||
_LOGGER.info("At least 1 target is required")
|
||||
return
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
message_attributes = {k: {"StringValue": json.dumps(v),
|
||||
"DataType": "String"}
|
||||
for k, v in kwargs.items() if v}
|
||||
for target in targets:
|
||||
self.client.publish(TargetArn=target, Message=message,
|
||||
Subject=kwargs.get(ATTR_TITLE),
|
||||
MessageAttributes=message_attributes)
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
AWS SQS platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.aws_sqs/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_PLATFORM, CONF_NAME)
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, BaseNotificationService)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ["boto3==1.3.1"]
|
||||
|
||||
CONF_REGION = "region_name"
|
||||
CONF_ACCESS_KEY_ID = "aws_access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
|
||||
CONF_PROFILE_NAME = "profile_name"
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "aws_sqs",
|
||||
vol.Optional(CONF_NAME): vol.Coerce(str),
|
||||
vol.Optional(CONF_REGION, default="us-east-1"): vol.Coerce(str),
|
||||
vol.Inclusive(CONF_ACCESS_KEY_ID, "credentials"): vol.Coerce(str),
|
||||
vol.Inclusive(CONF_SECRET_ACCESS_KEY, "credentials"): vol.Coerce(str),
|
||||
vol.Exclusive(CONF_PROFILE_NAME, "credentials"): vol.Coerce(str)
|
||||
})
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the AWS SQS notification service."""
|
||||
# pylint: disable=import-error
|
||||
import boto3
|
||||
|
||||
aws_config = config.copy()
|
||||
|
||||
del aws_config[CONF_PLATFORM]
|
||||
del aws_config[CONF_NAME]
|
||||
|
||||
profile = aws_config.get(CONF_PROFILE_NAME)
|
||||
|
||||
if profile is not None:
|
||||
boto3.setup_default_session(profile_name=profile)
|
||||
del aws_config[CONF_PROFILE_NAME]
|
||||
|
||||
sqs_client = boto3.client("sqs", **aws_config)
|
||||
|
||||
return AWSSQS(sqs_client)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class AWSSQS(BaseNotificationService):
|
||||
"""Implement the notification service for the AWS SQS service."""
|
||||
|
||||
def __init__(self, sqs_client):
|
||||
"""Initialize the service."""
|
||||
self.client = sqs_client
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send notification to specified SQS ARN."""
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
|
||||
if not targets:
|
||||
_LOGGER.info("At least 1 target is required")
|
||||
return
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
for target in targets:
|
||||
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
|
||||
message_body = {"message": message}
|
||||
message_body.update(cleaned_kwargs)
|
||||
message_attributes = {}
|
||||
for key, val in cleaned_kwargs.items():
|
||||
message_attributes[key] = {"StringValue": json.dumps(val),
|
||||
"DataType": "String"}
|
||||
self.client.send_message(QueueUrl=target,
|
||||
MessageBody=json.dumps(message_body),
|
||||
MessageAttributes=message_attributes)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Support for ecobee Send Message service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.ecobee/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components import ecobee
|
||||
from homeassistant.components.notify import BaseNotificationService
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the Ecobee notification service."""
|
||||
index = int(config['index']) if 'index' in config else 0
|
||||
return EcobeeNotificationService(index)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class EcobeeNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for the Ecobee thermostat."""
|
||||
|
||||
def __init__(self, thermostat_index):
|
||||
"""Initialize the service."""
|
||||
self.thermostat_index = thermostat_index
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a command line."""
|
||||
ecobee.NETWORK.ecobee.send_message(self.thermostat_index, message)
|
||||
@@ -41,8 +41,9 @@ class GNTPNotificationService(BaseNotificationService):
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, app_name, app_icon, hostname, password, port):
|
||||
"""Initialize the service."""
|
||||
from gntp import notifier
|
||||
self.gntp = notifier.GrowlNotifier(
|
||||
import gntp.notifier
|
||||
import gntp.errors
|
||||
self.gntp = gntp.notifier.GrowlNotifier(
|
||||
applicationName=app_name,
|
||||
notifications=["Notification"],
|
||||
applicationIcon=app_icon,
|
||||
@@ -50,7 +51,11 @@ class GNTPNotificationService(BaseNotificationService):
|
||||
password=password,
|
||||
port=port
|
||||
)
|
||||
self.gntp.register()
|
||||
try:
|
||||
self.gntp.register()
|
||||
except gntp.errors.NetworkError:
|
||||
_LOGGER.error('Unable to register with the GNTP host.')
|
||||
return
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Google Voice SMS platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.free_mobile/
|
||||
https://home-assistant.io/components/notify.google_voice/
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
|
||||
CONF_SENDER = 'sender'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['messagebird==1.1.1']
|
||||
REQUIREMENTS = ['messagebird==1.2.0']
|
||||
|
||||
|
||||
def is_valid_sender(sender):
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.components.notify import (
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pushbullet.py==0.9.0']
|
||||
REQUIREMENTS = ['pushbullet.py==0.10.0']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['slacker==0.6.8']
|
||||
REQUIREMENTS = ['slacker==0.9.10']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class SlackNotificationService(BaseNotificationService):
|
||||
"""Send a message to a user."""
|
||||
import slacker
|
||||
|
||||
channel = kwargs.get('channel', self._default_channel)
|
||||
channel = kwargs.get('target') or self._default_channel
|
||||
try:
|
||||
self.slack.chat.post_message(channel, message)
|
||||
except slacker.Error:
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-telegram-bot==3.4']
|
||||
REQUIREMENTS = ['python-telegram-bot==4.1.1']
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Twilio SMS platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.twilio_sms/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, DOMAIN, BaseNotificationService)
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ["twilio==5.4.0"]
|
||||
|
||||
CONF_ACCOUNT_SID = "account_sid"
|
||||
CONF_AUTH_TOKEN = "auth_token"
|
||||
CONF_FROM_NUMBER = "from_number"
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the Twilio SMS notification service."""
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: [CONF_ACCOUNT_SID,
|
||||
CONF_AUTH_TOKEN,
|
||||
CONF_FROM_NUMBER]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
# pylint: disable=import-error
|
||||
from twilio.rest import TwilioRestClient
|
||||
|
||||
twilio_client = TwilioRestClient(config[CONF_ACCOUNT_SID],
|
||||
config[CONF_AUTH_TOKEN])
|
||||
|
||||
return TwilioSMSNotificationService(twilio_client,
|
||||
config[CONF_FROM_NUMBER])
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class TwilioSMSNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for the Twilio SMS service."""
|
||||
|
||||
def __init__(self, twilio_client, from_number):
|
||||
"""Initialize the service."""
|
||||
self.client = twilio_client
|
||||
self.from_number = from_number
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send SMS to specified target user cell."""
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
|
||||
if not targets:
|
||||
_LOGGER.info("At least 1 target is required")
|
||||
return
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
for target in targets:
|
||||
self.client.messages.create(to=target, body=message,
|
||||
from_=self.from_number)
|
||||
@@ -11,7 +11,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['TwitterAPI==2.3.6']
|
||||
REQUIREMENTS = ['TwitterAPI==2.4.1']
|
||||
|
||||
CONF_CONSUMER_KEY = "consumer_key"
|
||||
CONF_CONSUMER_SECRET = "consumer_secret"
|
||||
|
||||
@@ -10,6 +10,10 @@ from homeassistant.components.notify import (BaseNotificationService, DOMAIN)
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME)
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
|
||||
'/archive/v0.1.2.zip'
|
||||
'#pylgtv==0.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -35,7 +39,7 @@ def get_service(hass, config):
|
||||
except PyLGTVPairException:
|
||||
_LOGGER.error('Pairing failed.')
|
||||
return None
|
||||
except ConnectionRefusedError:
|
||||
except OSError:
|
||||
_LOGGER.error('Host unreachable.')
|
||||
return None
|
||||
|
||||
@@ -58,5 +62,5 @@ class LgWebOSNotificationService(BaseNotificationService):
|
||||
self._client.send_message(message)
|
||||
except PyLGTVPairException:
|
||||
_LOGGER.error('Pairing failed.')
|
||||
except ConnectionRefusedError:
|
||||
except OSError:
|
||||
_LOGGER.error('Host unreachable.')
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
Support for monitoring OctoPrint 3D printers.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/octoprint/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import time
|
||||
import requests
|
||||
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
DOMAIN = "octoprint"
|
||||
OCTOPRINT = None
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVER_SENSORS = 'octoprint.sensors'
|
||||
DISCOVER_BINARY_SENSORS = 'octoprint.binary_sensor'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up OctoPrint API."""
|
||||
if not validate_config(config, {DOMAIN: [CONF_API_KEY],
|
||||
DOMAIN: [CONF_HOST]},
|
||||
_LOGGER):
|
||||
return False
|
||||
|
||||
base_url = config[DOMAIN][CONF_HOST] + "/api/"
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
|
||||
global OCTOPRINT
|
||||
try:
|
||||
OCTOPRINT = OctoPrintAPI(base_url, api_key)
|
||||
OCTOPRINT.get("printer")
|
||||
OCTOPRINT.get("job")
|
||||
except requests.exceptions.RequestException as conn_err:
|
||||
_LOGGER.error("Error setting up OctoPrint API: %r", conn_err)
|
||||
return False
|
||||
|
||||
for component, discovery_service in (
|
||||
('sensor', DISCOVER_SENSORS),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS)):
|
||||
discovery.discover(hass, discovery_service, component=component,
|
||||
hass_config=config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class OctoPrintAPI(object):
|
||||
"""Simple JSON wrapper for OctoPrint's API."""
|
||||
|
||||
def __init__(self, api_url, key):
|
||||
"""Initialize OctoPrint API and set headers needed later."""
|
||||
self.api_url = api_url
|
||||
self.headers = {'content-type': 'application/json',
|
||||
'X-Api-Key': key}
|
||||
self.printer_last_reading = [{}, None]
|
||||
self.job_last_reading = [{}, None]
|
||||
|
||||
def get_tools(self):
|
||||
"""Get the dynamic list of tools that temperature is monitored on."""
|
||||
tools = self.printer_last_reading[0]['temperature']
|
||||
return tools.keys()
|
||||
|
||||
def get(self, endpoint):
|
||||
"""Send a get request, and return the response as a dict."""
|
||||
now = time.time()
|
||||
if endpoint == "job":
|
||||
last_time = self.job_last_reading[1]
|
||||
if last_time is not None:
|
||||
if now - last_time < 30.0:
|
||||
return self.job_last_reading[0]
|
||||
elif endpoint == "printer":
|
||||
last_time = self.printer_last_reading[1]
|
||||
if last_time is not None:
|
||||
if now - last_time < 30.0:
|
||||
return self.printer_last_reading[0]
|
||||
url = self.api_url + endpoint
|
||||
try:
|
||||
response = requests.get(url,
|
||||
headers=self.headers,
|
||||
timeout=30)
|
||||
response.raise_for_status()
|
||||
if endpoint == "job":
|
||||
self.job_last_reading[0] = response.json()
|
||||
self.job_last_reading[1] = time.time()
|
||||
elif endpoint == "printer":
|
||||
self.printer_last_reading[0] = response.json()
|
||||
self.printer_last_reading[1] = time.time()
|
||||
return response.json()
|
||||
except requests.exceptions.ConnectionError as conn_exc:
|
||||
_LOGGER.error("Failed to update OctoPrint status. Error: %s",
|
||||
conn_exc)
|
||||
raise
|
||||
|
||||
def update(self, sensor_type, end_point, group, tool=None):
|
||||
"""Return the value for sensor_type from the provided endpoint."""
|
||||
try:
|
||||
return get_value_from_json(self.get(end_point), sensor_type,
|
||||
group, tool)
|
||||
except requests.exceptions.ConnectionError:
|
||||
raise
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def get_value_from_json(json_dict, sensor_type, group, tool):
|
||||
"""Return the value for sensor_type from the JSON."""
|
||||
if group in json_dict:
|
||||
if sensor_type in json_dict[group]:
|
||||
if sensor_type == "target" and json_dict[sensor_type] is None:
|
||||
return 0
|
||||
else:
|
||||
return json_dict[group][sensor_type]
|
||||
elif tool is not None:
|
||||
if sensor_type in json_dict[group][tool]:
|
||||
return json_dict[group][tool][sensor_type]
|
||||
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Support for Qwikswitch devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/qwikswitch/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.components.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.3.zip'
|
||||
'#pyqwikswitch==0.3']
|
||||
DEPENDENCIES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'qwikswitch'
|
||||
QSUSB = None
|
||||
|
||||
|
||||
class QSToggleEntity(object):
|
||||
"""Representation of a Qwikswitch Entity.
|
||||
|
||||
Implement base QS methods. Modeled around HA ToggleEntity[1] & should only
|
||||
be used in a class that extends both QSToggleEntity *and* ToggleEntity.
|
||||
|
||||
Implemented:
|
||||
- QSLight extends QSToggleEntity and Light[2] (ToggleEntity[1])
|
||||
- QSSwitch extends QSToggleEntity and SwitchDevice[3] (ToggleEntity[1])
|
||||
|
||||
[1] /helpers/entity.py
|
||||
[2] /components/light/__init__.py
|
||||
[3] /components/switch/__init__.py
|
||||
"""
|
||||
|
||||
def __init__(self, qsitem, qsusb):
|
||||
"""Initialize the ToggleEntity."""
|
||||
from pyqwikswitch import (QS_ID, QS_NAME, QSType, PQS_VALUE, PQS_TYPE)
|
||||
self._id = qsitem[QS_ID]
|
||||
self._name = qsitem[QS_NAME]
|
||||
self._value = qsitem[PQS_VALUE]
|
||||
self._qsusb = qsusb
|
||||
self._dim = qsitem[PQS_TYPE] == QSType.dimmer
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..100."""
|
||||
return self._value if self._dim else None
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the light."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Check if device is on (non-zero)."""
|
||||
return self._value > 0
|
||||
|
||||
def update_value(self, value):
|
||||
"""Decode the QSUSB value and update the Home assistant state."""
|
||||
if value != self._value:
|
||||
self._value = value
|
||||
# pylint: disable=no-member
|
||||
super().update_ha_state() # Part of Entity/ToggleEntity
|
||||
return self._value
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
newvalue = 255
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
newvalue = kwargs[ATTR_BRIGHTNESS]
|
||||
if self._qsusb.set(self._id, round(min(newvalue, 255)/2.55)) >= 0:
|
||||
self.update_value(newvalue)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
if self._qsusb.set(self._id, 0) >= 0:
|
||||
self.update_value(0)
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def setup(hass, config):
|
||||
"""Setup the QSUSB component."""
|
||||
from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD,
|
||||
QS_TYPE, PQS_VALUE, PQS_TYPE, QSType)
|
||||
|
||||
# Override which cmd's in /&listen packets will fire events
|
||||
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
|
||||
cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS))
|
||||
cmd_buttons = cmd_buttons.split(',')
|
||||
|
||||
try:
|
||||
url = config[DOMAIN].get('url', 'http://127.0.0.1:2020')
|
||||
dimmer_adjust = float(config[DOMAIN].get('dimmer_adjust', '1'))
|
||||
qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
|
||||
|
||||
# Ensure qsusb terminates threads correctly
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
lambda event: qsusb.stop())
|
||||
except ValueError as val_err:
|
||||
_LOGGER.error(str(val_err))
|
||||
return False
|
||||
|
||||
qsusb.ha_devices = qsusb.devices()
|
||||
qsusb.ha_objects = {}
|
||||
|
||||
# Identify switches & remove ' Switch' postfix in name
|
||||
for item in qsusb.ha_devices:
|
||||
if item[PQS_TYPE] == QSType.relay and \
|
||||
item[QS_NAME].lower().endswith(' switch'):
|
||||
item[QS_TYPE] = 'switch'
|
||||
item[QS_NAME] = item[QS_NAME][:-7]
|
||||
|
||||
global QSUSB
|
||||
if QSUSB is None:
|
||||
QSUSB = {}
|
||||
QSUSB[id(qsusb)] = qsusb
|
||||
|
||||
# Load sub-components for qwikswitch
|
||||
for comp_name in ('switch', 'light'):
|
||||
load_platform(hass, comp_name, 'qwikswitch',
|
||||
{'qsusb_id': id(qsusb)}, config)
|
||||
|
||||
def qs_callback(item):
|
||||
"""Typically a button press or update signal."""
|
||||
# If button pressed, fire a hass event
|
||||
if item.get(QS_CMD, '') in cmd_buttons:
|
||||
hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id'))
|
||||
return
|
||||
|
||||
# Update all ha_objects
|
||||
qsreply = qsusb.devices()
|
||||
if qsreply is False:
|
||||
return
|
||||
for item in qsreply:
|
||||
if item[QS_ID] in qsusb.ha_objects:
|
||||
qsusb.ha_objects[item[QS_ID]].update_value(
|
||||
round(min(item[PQS_VALUE], 100) * 2.55))
|
||||
|
||||
qsusb.listen(callback=qs_callback, timeout=30)
|
||||
return True
|
||||
@@ -13,7 +13,8 @@ import logging
|
||||
import queue
|
||||
import sqlite3
|
||||
import threading
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
@@ -21,6 +22,7 @@ from homeassistant.const import (
|
||||
EVENT_TIME_CHANGED, MATCH_ALL)
|
||||
from homeassistant.core import Event, EventOrigin, State
|
||||
from homeassistant.remote import JSONEncoder
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
|
||||
DOMAIN = "recorder"
|
||||
|
||||
@@ -30,6 +32,15 @@ RETURN_ROWCOUNT = "rowcount"
|
||||
RETURN_LASTROWID = "lastrowid"
|
||||
RETURN_ONE_ROW = "one_row"
|
||||
|
||||
CONF_PURGE_DAYS = "purge_days"
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1)),
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
_INSTANCE = None
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -102,14 +113,14 @@ def setup(hass, config):
|
||||
"""Setup the recorder."""
|
||||
# pylint: disable=global-statement
|
||||
global _INSTANCE
|
||||
|
||||
_INSTANCE = Recorder(hass)
|
||||
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
|
||||
_INSTANCE = Recorder(hass, purge_days=purge_days)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class RecorderRun(object):
|
||||
"""Representation of arecorder run."""
|
||||
"""Representation of a recorder run."""
|
||||
|
||||
def __init__(self, row=None):
|
||||
"""Initialize the recorder run."""
|
||||
@@ -169,11 +180,12 @@ class Recorder(threading.Thread):
|
||||
"""A threaded recorder class."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, hass):
|
||||
def __init__(self, hass, purge_days):
|
||||
"""Initialize the recorder."""
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.hass = hass
|
||||
self.purge_days = purge_days
|
||||
self.conn = None
|
||||
self.queue = queue.Queue()
|
||||
self.quit_object = object()
|
||||
@@ -194,6 +206,10 @@ class Recorder(threading.Thread):
|
||||
"""Start processing events to save."""
|
||||
self._setup_connection()
|
||||
self._setup_run()
|
||||
if self.purge_days is not None:
|
||||
track_point_in_utc_time(self.hass,
|
||||
lambda now: self._purge_old_data(),
|
||||
dt_util.utcnow() + timedelta(minutes=5))
|
||||
|
||||
while True:
|
||||
event = self.queue.get()
|
||||
@@ -475,6 +491,32 @@ class Recorder(threading.Thread):
|
||||
"UPDATE recorder_runs SET end=? WHERE start=?",
|
||||
(dt_util.utcnow(), self.recording_start))
|
||||
|
||||
def _purge_old_data(self):
|
||||
"""Purge events and states older than purge_days ago."""
|
||||
if not self.purge_days or self.purge_days < 1:
|
||||
_LOGGER.debug("purge_days set to %s, will not purge any old data.",
|
||||
self.purge_days)
|
||||
return
|
||||
|
||||
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
|
||||
|
||||
_LOGGER.info("Purging events created before %s", purge_before)
|
||||
deleted_rows = self.query(
|
||||
sql_query="DELETE FROM events WHERE created < ?;",
|
||||
data=(int(purge_before.timestamp()),),
|
||||
return_value=RETURN_ROWCOUNT)
|
||||
_LOGGER.debug("Deleted %s events", deleted_rows)
|
||||
|
||||
_LOGGER.info("Purging states created before %s", purge_before)
|
||||
deleted_rows = self.query(
|
||||
sql_query="DELETE FROM states WHERE created < ?;",
|
||||
data=(int(purge_before.timestamp()),),
|
||||
return_value=RETURN_ROWCOUNT)
|
||||
_LOGGER.debug("Deleted %s states", deleted_rows)
|
||||
|
||||
# Execute sqlite vacuum command to free up space on disk
|
||||
self.query("VACUUM;")
|
||||
|
||||
|
||||
def _adapt_datetime(datetimestamp):
|
||||
"""Turn a datetime into an integer for in the DB."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user