mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
commit
b78765a41f
12
.coveragerc
12
.coveragerc
@ -38,6 +38,9 @@ omit =
|
||||
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
|
||||
|
||||
@ -111,18 +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
|
||||
@ -138,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
|
||||
@ -153,6 +163,7 @@ omit =
|
||||
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
|
||||
@ -163,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
|
||||
|
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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]).
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -83,3 +83,5 @@ 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 . .
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -215,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.
|
||||
|
||||
@ -240,7 +240,7 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
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:
|
||||
@ -278,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,
|
||||
@ -293,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)
|
||||
@ -304,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)
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.helpers import condition, config_validation as cv
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'numeric_state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
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,
|
||||
@ -41,7 +41,7 @@ def trigger(hass, config, action):
|
||||
variables = {
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity_id,
|
||||
'entity_id': entity,
|
||||
'below': below,
|
||||
'above': above,
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -15,10 +15,12 @@ from homeassistant.const import (
|
||||
EVENT_PLATFORM_DISCOVERED)
|
||||
|
||||
DOMAIN = "discovery"
|
||||
REQUIREMENTS = ['netdisco==0.6.6']
|
||||
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__)
|
||||
|
@ -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,2 +1,2 @@
|
||||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
||||
VERSION = "77c51c270b0241ce7ba0d1df2d254d6f"
|
||||
VERSION = "0a226e905af198b2dabf1ce154844568"
|
||||
|
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
Subproject commit 6a8e6a5a081415690bf89e87697d15b6ce9ebf8b
|
||||
Subproject commit 4a667eb77e28a27dc766ca6f7bbd04e3866124d9
|
@ -1 +1 @@
|
||||
!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=192)}({192: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})})}))})}});
|
||||
!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})})}))})}});
|
@ -29,7 +29,7 @@ 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 = "set_swing_mode"
|
||||
SERVICE_SET_SWING_MODE = "set_swing_mode"
|
||||
SERVICE_SET_HUMIDITY = "set_humidity"
|
||||
|
||||
STATE_HEAT = "heat"
|
||||
@ -40,17 +40,17 @@ STATE_DRY = "dry"
|
||||
STATE_FAN_ONLY = "fan_only"
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = "current_temperature"
|
||||
ATTR_CURRENT_HUMIDITY = "current_humidity"
|
||||
ATTR_HUMIDITY = "humidity"
|
||||
ATTR_AWAY_MODE = "away_mode"
|
||||
ATTR_AUX_HEAT = "aux_heat"
|
||||
ATTR_FAN = "fan"
|
||||
ATTR_FAN_LIST = "fan_list"
|
||||
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 = "operation_mode"
|
||||
ATTR_OPERATION_MODE = "operation_mode"
|
||||
ATTR_OPERATION_LIST = "operation_list"
|
||||
ATTR_SWING_MODE = "swing_mode"
|
||||
ATTR_SWING_LIST = "swing_list"
|
||||
@ -108,7 +108,7 @@ def set_humidity(hass, humidity, entity_id=None):
|
||||
|
||||
def set_fan_mode(hass, fan, entity_id=None):
|
||||
"""Turn all or specified hvac fan mode on."""
|
||||
data = {ATTR_FAN: fan}
|
||||
data = {ATTR_FAN_MODE: fan}
|
||||
|
||||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
@ -118,7 +118,7 @@ def set_fan_mode(hass, fan, entity_id=None):
|
||||
|
||||
def set_operation_mode(hass, operation_mode, entity_id=None):
|
||||
"""Set new target operation mode."""
|
||||
data = {ATTR_OPERATION: operation_mode}
|
||||
data = {ATTR_OPERATION_MODE: operation_mode}
|
||||
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
@ -133,7 +133,7 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_SWING, data)
|
||||
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
@ -247,12 +247,12 @@ def setup(hass, config):
|
||||
"""Set fan mode on target hvacs."""
|
||||
target_hvacs = component.extract_from_service(service)
|
||||
|
||||
fan = service.data.get(ATTR_FAN)
|
||||
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)
|
||||
SERVICE_SET_FAN_MODE, ATTR_FAN_MODE)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
@ -269,16 +269,16 @@ def setup(hass, config):
|
||||
"""Set operating mode on the target hvacs."""
|
||||
target_hvacs = component.extract_from_service(service)
|
||||
|
||||
operation_mode = service.data.get(ATTR_OPERATION)
|
||||
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)
|
||||
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
hvac.set_operation(operation_mode)
|
||||
hvac.set_operation_mode(operation_mode)
|
||||
|
||||
if hvac.should_poll:
|
||||
hvac.update_ha_state(True)
|
||||
@ -296,18 +296,18 @@ def setup(hass, config):
|
||||
if swing_mode is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_SWING, ATTR_SWING_MODE)
|
||||
SERVICE_SET_SWING_MODE, ATTR_SWING_MODE)
|
||||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
hvac.set_swing(swing_mode)
|
||||
hvac.set_swing_mode(swing_mode)
|
||||
|
||||
if hvac.should_poll:
|
||||
hvac.update_ha_state(True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_SWING, swing_set_service,
|
||||
descriptions.get(SERVICE_SET_SWING))
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
|
||||
descriptions.get(SERVICE_SET_SWING_MODE))
|
||||
return True
|
||||
|
||||
|
||||
@ -330,19 +330,30 @@ class HvacDevice(Entity):
|
||||
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
|
||||
ATTR_TEMPERATURE:
|
||||
self._convert_for_display(self.target_temperature),
|
||||
ATTR_HUMIDITY: self.target_humidity,
|
||||
ATTR_CURRENT_HUMIDITY: self.current_humidity,
|
||||
ATTR_MIN_HUMIDITY: self.min_humidity,
|
||||
ATTR_MAX_HUMIDITY: self.max_humidity,
|
||||
ATTR_FAN_LIST: self.fan_list,
|
||||
ATTR_OPERATION_LIST: self.operation_list,
|
||||
ATTR_SWING_LIST: self.swing_list,
|
||||
ATTR_OPERATION: self.current_operation,
|
||||
ATTR_FAN: self.current_fan_mode,
|
||||
ATTR_SWING_MODE: self.current_swing_mode,
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
@ -430,11 +441,11 @@ class HvacDevice(Entity):
|
||||
"""Set new target fan mode."""
|
||||
pass
|
||||
|
||||
def set_operation(self, operation_mode):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
pass
|
||||
|
||||
def set_swing(self, swing_mode):
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
pass
|
||||
|
||||
@ -457,12 +468,12 @@ class HvacDevice(Entity):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._convert_for_display(7)
|
||||
return convert(7, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._convert_for_display(35)
|
||||
return convert(35, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
|
||||
@property
|
||||
def min_humidity(self):
|
||||
|
@ -118,7 +118,7 @@ class DemoHvac(HvacDevice):
|
||||
self._target_humidity = humidity
|
||||
self.update_ha_state()
|
||||
|
||||
def set_swing(self, swing_mode):
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target temperature."""
|
||||
self._current_swing_mode = swing_mode
|
||||
self.update_ha_state()
|
||||
@ -128,7 +128,7 @@ class DemoHvac(HvacDevice):
|
||||
self._current_fan_mode = fan
|
||||
self.update_ha_state()
|
||||
|
||||
def set_operation(self, operation_mode):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target temperature."""
|
||||
self._current_operation = operation_mode
|
||||
self.update_ha_state()
|
||||
|
45
homeassistant/components/hvac/zwave.py
Normal file → Executable file
45
homeassistant/components/hvac/zwave.py
Normal file → Executable file
@ -1,5 +1,9 @@
|
||||
"""ZWave Hvac device."""
|
||||
"""
|
||||
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
|
||||
@ -19,6 +23,12 @@ 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 = {
|
||||
@ -96,22 +106,24 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
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=0x43).values():
|
||||
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=0x40).values():
|
||||
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=0x31).values():
|
||||
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
|
||||
fan_class_id = 0x44 if self._zxt_120 else 0x42
|
||||
_LOGGER.debug("fan_class_id=%s", fan_class_id)
|
||||
for value in self._node.get_values(class_id=fan_class_id).values():
|
||||
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)
|
||||
@ -119,7 +131,8 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
self._current_operation_state)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in self._node.get_values(class_id=0x70).values():
|
||||
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]
|
||||
@ -184,7 +197,8 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
for value in self._node.get_values(class_id=0x43).values():
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
if value.command_class != 67:
|
||||
continue
|
||||
if self._zxt_120:
|
||||
@ -200,20 +214,23 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
for value in self._node.get_values(class_id=0x44).values():
|
||||
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(self, operation_mode):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
for value in self._node.get_values(class_id=0x40).values():
|
||||
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(self, swing_mode):
|
||||
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=0x70).values():
|
||||
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)
|
||||
|
||||
|
@ -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]
|
||||
|
35
homeassistant/components/light/qwikswitch.py
Normal file
35
homeassistant/components/light/qwikswitch.py
Normal file
@ -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]'
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
61
homeassistant/components/logentries.py
Normal file
61
homeassistant/components/logentries.py
Normal file
@ -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
|
@ -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'
|
||||
@ -144,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({
|
||||
@ -255,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}
|
||||
@ -263,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)
|
||||
|
||||
|
||||
@ -363,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)
|
||||
|
@ -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()
|
||||
|
158
homeassistant/components/media_player/gpmdp.py
Normal file
158
homeassistant/components/media_player/gpmdp.py
Normal file
@ -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)
|
||||
|
@ -278,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}, {})
|
||||
|
210
homeassistant/components/media_player/lg_netcast.py
Normal file
210
homeassistant/components/media_player/lg_netcast.py
Normal file
@ -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))
|
||||
|
187
homeassistant/components/media_player/roku.py
Normal file
187
homeassistant/components/media_player/roku.py
Normal file
@ -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()
|
@ -145,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'
|
||||
|
@ -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):
|
||||
|
@ -402,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}
|
||||
|
@ -388,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,
|
||||
|
@ -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."""
|
||||
|
91
homeassistant/components/notify/aws_lambda.py
Normal file
91
homeassistant/components/notify/aws_lambda.py
Normal file
@ -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)
|
80
homeassistant/components/notify/aws_sns.py
Normal file
80
homeassistant/components/notify/aws_sns.py
Normal file
@ -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)
|
84
homeassistant/components/notify/aws_sqs.py
Normal file
84
homeassistant/components/notify/aws_sqs.py
Normal file
@ -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)
|
31
homeassistant/components/notify/ecobee.py
Normal file
31
homeassistant/components/notify/ecobee.py
Normal file
@ -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
|
||||
|
||||
|
@ -51,7 +51,7 @@ class SlackNotificationService(BaseNotificationService):
|
||||
"""Send a message to a user."""
|
||||
import slacker
|
||||
|
||||
channel = kwargs.get('target', 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==4.0.1']
|
||||
REQUIREMENTS = ['python-telegram-bot==4.1.1']
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
|
62
homeassistant/components/notify/twilio_sms.py
Normal file
62
homeassistant/components/notify/twilio_sms.py
Normal file
@ -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)
|
150
homeassistant/components/qwikswitch.py
Normal file
150
homeassistant/components/qwikswitch.py
Normal file
@ -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."""
|
||||
|
@ -2,7 +2,7 @@
|
||||
Support for Powerview scenes from a Powerview hub.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/scene/
|
||||
https://home-assistant.io/components/scene.hunterdouglas_powerview/
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
@ -10,7 +10,7 @@ import logging
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from homeassistant.const import HTTP_OK
|
||||
from homeassistant.const import HTTP_OK, TEMP_CELSIUS
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
@ -236,7 +236,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
dev = []
|
||||
for resource in config.get("monitored_resources",
|
||||
FITBIT_DEFAULT_RESOURCE_LIST):
|
||||
dev.append(FitbitSensor(authd_client, config_path, resource))
|
||||
dev.append(FitbitSensor(authd_client, config_path, resource,
|
||||
hass.config.temperature_unit ==
|
||||
TEMP_CELSIUS))
|
||||
add_devices(dev)
|
||||
|
||||
else:
|
||||
@ -314,8 +316,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class FitbitSensor(Entity):
|
||||
"""Implementation of a Fitbit sensor."""
|
||||
|
||||
def __init__(self, client, config_path, resource_type):
|
||||
"""Initialize the Uber sensor."""
|
||||
def __init__(self, client, config_path, resource_type, is_metric):
|
||||
"""Initialize the Fitbit sensor."""
|
||||
self.client = client
|
||||
self.config_path = config_path
|
||||
self.resource_type = resource_type
|
||||
@ -328,7 +330,13 @@ class FitbitSensor(Entity):
|
||||
unit_type = FITBIT_RESOURCES_LIST[self.resource_type]
|
||||
if unit_type == "":
|
||||
split_resource = self.resource_type.split("/")
|
||||
measurement_system = FITBIT_MEASUREMENTS[self.client.system]
|
||||
try:
|
||||
measurement_system = FITBIT_MEASUREMENTS[self.client.system]
|
||||
except KeyError:
|
||||
if is_metric:
|
||||
measurement_system = FITBIT_MEASUREMENTS["metric"]
|
||||
else:
|
||||
measurement_system = FITBIT_MEASUREMENTS["en_US"]
|
||||
unit_type = measurement_system[split_resource[-1]]
|
||||
self._unit_of_measurement = unit_type
|
||||
self._state = 0
|
||||
|
@ -10,8 +10,10 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS
|
||||
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -23,47 +25,102 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
CONF_ORIGIN = 'origin'
|
||||
CONF_DESTINATION = 'destination'
|
||||
CONF_TRAVEL_MODE = 'travel_mode'
|
||||
CONF_OPTIONS = 'options'
|
||||
CONF_MODE = 'mode'
|
||||
CONF_NAME = 'name'
|
||||
|
||||
ALL_LANGUAGES = ['ar', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es',
|
||||
'eu', 'fa', 'fi', 'fr', 'gl', 'gu', 'hi', 'hr', 'hu', 'id',
|
||||
'it', 'iw', 'ja', 'kn', 'ko', 'lt', 'lv', 'ml', 'mr', 'nl',
|
||||
'no', 'pl', 'pt', 'pt-BR', 'pt-PT', 'ro', 'ru', 'sk', 'sl',
|
||||
'sr', 'sv', 'ta', 'te', 'th', 'tl', 'tr', 'uk', 'vi',
|
||||
'zh-CN', 'zh-TW']
|
||||
|
||||
TRANSIT_PREFS = ['less_walking', 'fewer_transfers']
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required('platform'): 'google_travel_time',
|
||||
vol.Optional(CONF_NAME): vol.Coerce(str),
|
||||
vol.Required(CONF_API_KEY): vol.Coerce(str),
|
||||
vol.Required(CONF_ORIGIN): vol.Coerce(str),
|
||||
vol.Required(CONF_DESTINATION): vol.Coerce(str),
|
||||
vol.Optional(CONF_TRAVEL_MODE, default='driving'):
|
||||
vol.In(["driving", "walking", "bicycling", "transit"])
|
||||
vol.Optional(CONF_TRAVEL_MODE):
|
||||
vol.In(["driving", "walking", "bicycling", "transit"]),
|
||||
vol.Optional(CONF_OPTIONS, default=dict()): vol.All(
|
||||
dict, vol.Schema({
|
||||
vol.Optional(CONF_MODE, default='driving'):
|
||||
vol.In(["driving", "walking", "bicycling", "transit"]),
|
||||
vol.Optional('language'): vol.In(ALL_LANGUAGES),
|
||||
vol.Optional('avoid'): vol.In(['tolls', 'highways',
|
||||
'ferries', 'indoor']),
|
||||
vol.Optional('units'): vol.In(['metric', 'imperial']),
|
||||
vol.Exclusive('arrival_time', 'time'): cv.string,
|
||||
vol.Exclusive('departure_time', 'time'): cv.string,
|
||||
vol.Optional('traffic_model'): vol.In(['best_guess',
|
||||
'pessimistic',
|
||||
'optimistic']),
|
||||
vol.Optional('transit_mode'): vol.In(['bus', 'subway', 'train',
|
||||
'tram', 'rail']),
|
||||
vol.Optional('transit_routing_preference'): vol.In(TRANSIT_PREFS)
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
def convert_time_to_utc(timestr):
|
||||
"""Take a string like 08:00:00 and convert it to a unix timestamp."""
|
||||
combined = datetime.combine(dt_util.start_of_local_day(),
|
||||
dt_util.parse_time(timestr))
|
||||
if combined < datetime.now():
|
||||
combined = combined + timedelta(days=1)
|
||||
return dt_util.as_timestamp(combined)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the travel time platform."""
|
||||
# pylint: disable=too-many-locals
|
||||
options = config.get(CONF_OPTIONS)
|
||||
|
||||
is_metric = (hass.config.temperature_unit == TEMP_CELSIUS)
|
||||
if options.get('units') is None:
|
||||
if hass.config.temperature_unit is TEMP_CELSIUS:
|
||||
options['units'] = 'metric'
|
||||
elif hass.config.temperature_unit is TEMP_FAHRENHEIT:
|
||||
options['units'] = 'imperial'
|
||||
|
||||
travel_mode = config.get(CONF_TRAVEL_MODE)
|
||||
mode = options.get(CONF_MODE)
|
||||
|
||||
if travel_mode is not None:
|
||||
wstr = ("Google Travel Time: travel_mode is deprecated, please add "
|
||||
"mode to the options dictionary instead!")
|
||||
_LOGGER.warning(wstr)
|
||||
if mode is None:
|
||||
options[CONF_MODE] = travel_mode
|
||||
|
||||
titled_mode = options.get(CONF_MODE).title()
|
||||
formatted_name = "Google Travel Time - {}".format(titled_mode)
|
||||
name = config.get(CONF_NAME, formatted_name)
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
origin = config.get(CONF_ORIGIN)
|
||||
destination = config.get(CONF_DESTINATION)
|
||||
travel_mode = config.get(CONF_TRAVEL_MODE)
|
||||
|
||||
sensor = GoogleTravelTimeSensor(api_key, origin, destination,
|
||||
travel_mode, is_metric)
|
||||
sensor = GoogleTravelTimeSensor(name, api_key, origin, destination,
|
||||
options)
|
||||
|
||||
if sensor.valid_api_connection:
|
||||
add_devices_callback([sensor])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GoogleTravelTimeSensor(Entity):
|
||||
"""Representation of a tavel time sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, api_key, origin, destination, travel_mode, is_metric):
|
||||
def __init__(self, name, api_key, origin, destination, options):
|
||||
"""Initialize the sensor."""
|
||||
if is_metric:
|
||||
self._unit = 'metric'
|
||||
else:
|
||||
self._unit = 'imperial'
|
||||
self._name = name
|
||||
self._options = options
|
||||
self._origin = origin
|
||||
self._destination = destination
|
||||
self._travel_mode = travel_mode
|
||||
self._matrix = None
|
||||
self.valid_api_connection = True
|
||||
|
||||
@ -79,17 +136,22 @@ class GoogleTravelTimeSensor(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._matrix['rows'][0]['elements'][0]['duration']['value']/60.0
|
||||
try:
|
||||
res = self._matrix['rows'][0]['elements'][0]['duration']['value']
|
||||
return round(res/60)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Get the name of the sensor."""
|
||||
return "Google Travel time"
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
res = self._matrix.copy()
|
||||
res.update(self._options)
|
||||
del res['rows']
|
||||
_data = self._matrix['rows'][0]['elements'][0]
|
||||
if 'duration_in_traffic' in _data:
|
||||
@ -108,10 +170,15 @@ class GoogleTravelTimeSensor(Entity):
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from Google."""
|
||||
now = datetime.now()
|
||||
options_copy = self._options.copy()
|
||||
dtime = options_copy.get('departure_time')
|
||||
atime = options_copy.get('arrival_time')
|
||||
if dtime is not None and ':' in dtime:
|
||||
options_copy['departure_time'] = convert_time_to_utc(dtime)
|
||||
|
||||
if atime is not None and ':' in atime:
|
||||
options_copy['arrival_time'] = convert_time_to_utc(atime)
|
||||
|
||||
self._matrix = self._client.distance_matrix(self._origin,
|
||||
self._destination,
|
||||
mode=self._travel_mode,
|
||||
units=self._unit,
|
||||
departure_time=now,
|
||||
traffic_model="optimistic")
|
||||
**options_copy)
|
||||
|
@ -7,14 +7,15 @@ https://home-assistant.io/components/sensor.gtfs/
|
||||
import os
|
||||
import logging
|
||||
import datetime
|
||||
import threading
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ["https://github.com/robbiet480/pygtfs/archive/"
|
||||
"432414b720c580fb2667a0a48f539118a2d95969.zip#"
|
||||
"pygtfs==0.1.2"]
|
||||
"00546724e4bbcb3053110d844ca44e2246267dd8.zip#"
|
||||
"pygtfs==0.1.3"]
|
||||
|
||||
ICON = "mdi:train"
|
||||
|
||||
@ -152,9 +153,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error("The given GTFS data file/folder was not found!")
|
||||
return False
|
||||
|
||||
import pygtfs
|
||||
|
||||
split_file_name = os.path.splitext(config["data"])
|
||||
|
||||
sqlite_file = "{}.sqlite".format(split_file_name[0])
|
||||
joined_path = os.path.join(gtfs_dir, sqlite_file)
|
||||
gtfs = pygtfs.Schedule(joined_path)
|
||||
|
||||
# pylint: disable=no-member
|
||||
if len(gtfs.feeds) < 1:
|
||||
pygtfs.append_feed(gtfs, os.path.join(gtfs_dir,
|
||||
config["data"]))
|
||||
|
||||
dev = []
|
||||
dev.append(GTFSDepartureSensor(config["data"], gtfs_dir,
|
||||
config["origin"], config["destination"]))
|
||||
dev.append(GTFSDepartureSensor(gtfs, config["origin"],
|
||||
config["destination"]))
|
||||
add_devices(dev)
|
||||
|
||||
# pylint: disable=too-many-instance-attributes,too-few-public-methods
|
||||
@ -163,16 +177,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class GTFSDepartureSensor(Entity):
|
||||
"""Implementation of an GTFS departures sensor."""
|
||||
|
||||
def __init__(self, data_source, gtfs_folder, origin, destination):
|
||||
def __init__(self, pygtfs, origin, destination):
|
||||
"""Initialize the sensor."""
|
||||
self._data_source = data_source
|
||||
self._gtfs_folder = gtfs_folder
|
||||
self._pygtfs = pygtfs
|
||||
self.origin = origin
|
||||
self.destination = destination
|
||||
self._name = "GTFS Sensor"
|
||||
self._unit_of_measurement = "min"
|
||||
self._state = 0
|
||||
self._attributes = {}
|
||||
self.lock = threading.Lock()
|
||||
self.update()
|
||||
|
||||
@property
|
||||
@ -202,62 +216,52 @@ class GTFSDepartureSensor(Entity):
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from GTFS and update the states."""
|
||||
import pygtfs
|
||||
with self.lock:
|
||||
self._departure = get_next_departure(self._pygtfs, self.origin,
|
||||
self.destination)
|
||||
self._state = self._departure["minutes_until_departure"]
|
||||
|
||||
split_file_name = os.path.splitext(self._data_source)
|
||||
origin_station = self._departure["origin_station"]
|
||||
destination_station = self._departure["destination_station"]
|
||||
origin_stop_time = self._departure["origin_stop_time"]
|
||||
destination_stop_time = self._departure["destination_stop_time"]
|
||||
agency = self._departure["agency"]
|
||||
route = self._departure["route"]
|
||||
trip = self._departure["trip"]
|
||||
|
||||
sqlite_file = "{}.sqlite".format(split_file_name[0])
|
||||
gtfs = pygtfs.Schedule(os.path.join(self._gtfs_folder, sqlite_file))
|
||||
name = "{} {} to {} next departure"
|
||||
self._name = name.format(agency.agency_name,
|
||||
origin_station.stop_id,
|
||||
destination_station.stop_id)
|
||||
|
||||
# pylint: disable=no-member
|
||||
if len(gtfs.feeds) < 1:
|
||||
pygtfs.append_feed(gtfs, os.path.join(self._gtfs_folder,
|
||||
self._data_source))
|
||||
# Build attributes
|
||||
|
||||
self._departure = get_next_departure(gtfs, self.origin,
|
||||
self.destination)
|
||||
self._state = self._departure["minutes_until_departure"]
|
||||
self._attributes = {}
|
||||
|
||||
origin_station = self._departure["origin_station"]
|
||||
destination_station = self._departure["destination_station"]
|
||||
origin_stop_time = self._departure["origin_stop_time"]
|
||||
destination_stop_time = self._departure["destination_stop_time"]
|
||||
agency = self._departure["agency"]
|
||||
route = self._departure["route"]
|
||||
trip = self._departure["trip"]
|
||||
def dict_for_table(resource):
|
||||
"""Return a dict for the SQLAlchemy resource given."""
|
||||
return dict((col, getattr(resource, col))
|
||||
for col in resource.__table__.columns.keys())
|
||||
|
||||
name = "{} {} to {} next departure"
|
||||
self._name = name.format(agency.agency_name,
|
||||
origin_station.stop_id,
|
||||
destination_station.stop_id)
|
||||
def append_keys(resource, prefix=None):
|
||||
"""Properly format key val pairs to append to attributes."""
|
||||
for key, val in resource.items():
|
||||
if val == "" or val is None or key == "feed_id":
|
||||
continue
|
||||
pretty_key = key.replace("_", " ")
|
||||
pretty_key = pretty_key.title()
|
||||
pretty_key = pretty_key.replace("Id", "ID")
|
||||
pretty_key = pretty_key.replace("Url", "URL")
|
||||
if prefix is not None and \
|
||||
pretty_key.startswith(prefix) is False:
|
||||
pretty_key = "{} {}".format(prefix, pretty_key)
|
||||
self._attributes[pretty_key] = val
|
||||
|
||||
# Build attributes
|
||||
|
||||
self._attributes = {}
|
||||
|
||||
def dict_for_table(resource):
|
||||
"""Return a dict for the SQLAlchemy resource given."""
|
||||
return dict((col, getattr(resource, col))
|
||||
for col in resource.__table__.columns.keys())
|
||||
|
||||
def append_keys(resource, prefix=None):
|
||||
"""Properly format key val pairs to append to attributes."""
|
||||
for key, val in resource.items():
|
||||
if val == "" or val is None or key == "feed_id":
|
||||
continue
|
||||
pretty_key = key.replace("_", " ")
|
||||
pretty_key = pretty_key.title()
|
||||
pretty_key = pretty_key.replace("Id", "ID")
|
||||
pretty_key = pretty_key.replace("Url", "URL")
|
||||
if prefix is not None and \
|
||||
pretty_key.startswith(prefix) is False:
|
||||
pretty_key = "{} {}".format(prefix, pretty_key)
|
||||
self._attributes[pretty_key] = val
|
||||
|
||||
append_keys(dict_for_table(agency), "Agency")
|
||||
append_keys(dict_for_table(route), "Route")
|
||||
append_keys(dict_for_table(trip), "Trip")
|
||||
append_keys(dict_for_table(origin_station), "Origin Station")
|
||||
append_keys(dict_for_table(destination_station), "Destination Station")
|
||||
append_keys(origin_stop_time, "Origin Stop")
|
||||
append_keys(destination_stop_time, "Destination Stop")
|
||||
append_keys(dict_for_table(agency), "Agency")
|
||||
append_keys(dict_for_table(route), "Route")
|
||||
append_keys(dict_for_table(trip), "Trip")
|
||||
append_keys(dict_for_table(origin_station), "Origin Station")
|
||||
append_keys(dict_for_table(destination_station),
|
||||
"Destination Station")
|
||||
append_keys(origin_stop_time, "Origin Stop")
|
||||
append_keys(destination_stop_time, "Destination Stop")
|
||||
|
90
homeassistant/components/sensor/lastfm.py
Normal file
90
homeassistant/components/sensor/lastfm.py
Normal file
@ -0,0 +1,90 @@
|
||||
"""
|
||||
Sensor for Last.fm account status.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.lastfm/
|
||||
"""
|
||||
import re
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
|
||||
ICON = 'mdi:lastfm'
|
||||
|
||||
REQUIREMENTS = ['pylast==1.6.0']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Last.fm platform."""
|
||||
import pylast as lastfm
|
||||
network = lastfm.LastFMNetwork(api_key=config.get(CONF_API_KEY))
|
||||
add_devices(
|
||||
[LastfmSensor(username,
|
||||
network) for username in config.get("users", [])])
|
||||
|
||||
|
||||
class LastfmSensor(Entity):
|
||||
"""A class for the Last.fm account."""
|
||||
|
||||
# pylint: disable=abstract-method, too-many-instance-attributes
|
||||
def __init__(self, user, lastfm):
|
||||
"""Initialize the sensor."""
|
||||
self._user = lastfm.get_user(user)
|
||||
self._name = user
|
||||
self._lastfm = lastfm
|
||||
self._state = "Not Scrobbling"
|
||||
self._playcount = None
|
||||
self._lastplayed = None
|
||||
self._topplayed = None
|
||||
self._cover = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""Return the entity ID."""
|
||||
return 'sensor.lastfm_{}'.format(self._name)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
# pylint: disable=no-member
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
self._cover = self._user.get_image()
|
||||
self._playcount = self._user.get_playcount()
|
||||
last = self._user.get_recent_tracks(limit=2)[0]
|
||||
self._lastplayed = "{} - {}".format(last.track.artist,
|
||||
last.track.title)
|
||||
top = self._user.get_top_tracks(limit=1)[0]
|
||||
toptitle = re.search("', '(.+?)',", str(top))
|
||||
topartist = re.search("'(.+?)',", str(top))
|
||||
self._topplayed = "{} - {}".format(topartist.group(1),
|
||||
toptitle.group(1))
|
||||
if self._user.get_now_playing() is None:
|
||||
self._state = "Not Scrobbling"
|
||||
return
|
||||
now = self._user.get_now_playing()
|
||||
self._state = "{} - {}".format(now.artist, now.title)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {'Play Count': self._playcount, 'Last Played':
|
||||
self._lastplayed, 'Top Played': self._topplayed}
|
||||
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Avatar of the user."""
|
||||
return self._cover
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return ICON
|
@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "loopenergy"
|
||||
|
||||
REQUIREMENTS = ['pyloopenergy==0.0.10']
|
||||
REQUIREMENTS = ['pyloopenergy==0.0.12']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
268
homeassistant/components/sensor/mold_indicator.py
Normal file
268
homeassistant/components/sensor/mold_indicator.py
Normal file
@ -0,0 +1,268 @@
|
||||
"""
|
||||
Calculates mold growth indication from temperature and humidity.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.mold_indicator/
|
||||
"""
|
||||
import logging
|
||||
import math
|
||||
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Mold Indicator"
|
||||
CONF_INDOOR_TEMP = "indoor_temp_sensor"
|
||||
CONF_OUTDOOR_TEMP = "outdoor_temp_sensor"
|
||||
CONF_INDOOR_HUMIDITY = "indoor_humidity_sensor"
|
||||
CONF_CALIBRATION_FACTOR = "calibration_factor"
|
||||
|
||||
MAGNUS_K2 = 17.62
|
||||
MAGNUS_K3 = 243.12
|
||||
|
||||
ATTR_DEWPOINT = "Dewpoint"
|
||||
ATTR_CRITICAL_TEMP = "Est. Crit. Temp"
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup MoldIndicator sensor."""
|
||||
name = config.get('name', DEFAULT_NAME)
|
||||
indoor_temp_sensor = config.get(CONF_INDOOR_TEMP)
|
||||
outdoor_temp_sensor = config.get(CONF_OUTDOOR_TEMP)
|
||||
indoor_humidity_sensor = config.get(CONF_INDOOR_HUMIDITY)
|
||||
calib_factor = util.convert(config.get(CONF_CALIBRATION_FACTOR),
|
||||
float, None)
|
||||
|
||||
if None in (indoor_temp_sensor,
|
||||
outdoor_temp_sensor, indoor_humidity_sensor):
|
||||
_LOGGER.error('Missing required key %s, %s or %s',
|
||||
CONF_INDOOR_TEMP, CONF_OUTDOOR_TEMP,
|
||||
CONF_INDOOR_HUMIDITY)
|
||||
return False
|
||||
|
||||
add_devices_callback([MoldIndicator(
|
||||
hass, name, indoor_temp_sensor,
|
||||
outdoor_temp_sensor, indoor_humidity_sensor,
|
||||
calib_factor)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class MoldIndicator(Entity):
|
||||
"""Represents a MoldIndication sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, indoor_temp_sensor, outdoor_temp_sensor,
|
||||
indoor_humidity_sensor, calib_factor):
|
||||
"""Initialize the sensor."""
|
||||
self._state = None
|
||||
self._name = name
|
||||
self._indoor_temp_sensor = indoor_temp_sensor
|
||||
self._indoor_humidity_sensor = indoor_humidity_sensor
|
||||
self._outdoor_temp_sensor = outdoor_temp_sensor
|
||||
self._calib_factor = calib_factor
|
||||
self._is_metric = (hass.config.temperature_unit == TEMP_CELSIUS)
|
||||
|
||||
self._dewpoint = None
|
||||
self._indoor_temp = None
|
||||
self._outdoor_temp = None
|
||||
self._indoor_hum = None
|
||||
self._crit_temp = None
|
||||
|
||||
track_state_change(hass, indoor_temp_sensor, self._sensor_changed)
|
||||
track_state_change(hass, outdoor_temp_sensor, self._sensor_changed)
|
||||
track_state_change(hass, indoor_humidity_sensor, self._sensor_changed)
|
||||
|
||||
# Read initial state
|
||||
indoor_temp = hass.states.get(indoor_temp_sensor)
|
||||
outdoor_temp = hass.states.get(outdoor_temp_sensor)
|
||||
indoor_hum = hass.states.get(indoor_humidity_sensor)
|
||||
|
||||
if indoor_temp:
|
||||
self._indoor_temp = \
|
||||
MoldIndicator._update_temp_sensor(indoor_temp)
|
||||
|
||||
if outdoor_temp:
|
||||
self._outdoor_temp = \
|
||||
MoldIndicator._update_temp_sensor(outdoor_temp)
|
||||
|
||||
if indoor_hum:
|
||||
self._indoor_hum = \
|
||||
MoldIndicator._update_hum_sensor(indoor_hum)
|
||||
|
||||
self.update()
|
||||
|
||||
@staticmethod
|
||||
def _update_temp_sensor(state):
|
||||
"""Parse temperature sensor value."""
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
temp = util.convert(state.state, float)
|
||||
|
||||
if temp is None:
|
||||
_LOGGER.error('Unable to parse sensor temperature: %s',
|
||||
state.state)
|
||||
return None
|
||||
|
||||
# convert to celsius if necessary
|
||||
if unit == TEMP_FAHRENHEIT:
|
||||
return util.temperature.fahrenheit_to_celcius(temp)
|
||||
elif unit == TEMP_CELSIUS:
|
||||
return temp
|
||||
else:
|
||||
_LOGGER.error("Temp sensor has unsupported unit: %s"
|
||||
" (allowed: %s, %s)",
|
||||
unit, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _update_hum_sensor(state):
|
||||
"""Parse humidity sensor value."""
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
hum = util.convert(state.state, float)
|
||||
|
||||
if hum is None:
|
||||
_LOGGER.error('Unable to parse sensor humidity: %s',
|
||||
state.state)
|
||||
return None
|
||||
|
||||
# check unit
|
||||
if unit != "%":
|
||||
_LOGGER.error(
|
||||
"Humidity sensor has unsupported unit: %s %s",
|
||||
unit,
|
||||
" (allowed: %)")
|
||||
|
||||
# check range
|
||||
if hum > 100 or hum < 0:
|
||||
_LOGGER.error(
|
||||
"Humidity sensor out of range: %s %s",
|
||||
hum,
|
||||
" (allowed: 0-100%)")
|
||||
|
||||
return hum
|
||||
|
||||
def update(self):
|
||||
"""Calculate latest state."""
|
||||
# check all sensors
|
||||
if None in (self._indoor_temp, self._indoor_hum, self._outdoor_temp):
|
||||
return
|
||||
|
||||
# re-calculate dewpoint and mold indicator
|
||||
self._calc_dewpoint()
|
||||
self._calc_moldindicator()
|
||||
|
||||
def _sensor_changed(self, entity_id, old_state, new_state):
|
||||
"""Called when sensor values change."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
if entity_id == self._indoor_temp_sensor:
|
||||
# update the indoor temp sensor
|
||||
self._indoor_temp = MoldIndicator._update_temp_sensor(new_state)
|
||||
|
||||
elif entity_id == self._outdoor_temp_sensor:
|
||||
# update outdoor temp sensor
|
||||
self._outdoor_temp = MoldIndicator._update_temp_sensor(new_state)
|
||||
|
||||
elif entity_id == self._indoor_humidity_sensor:
|
||||
# update humidity
|
||||
self._indoor_hum = MoldIndicator._update_hum_sensor(new_state)
|
||||
|
||||
self.update()
|
||||
self.update_ha_state()
|
||||
|
||||
def _calc_dewpoint(self):
|
||||
"""Calculate the dewpoint for the indoor air."""
|
||||
# use magnus approximation to calculate the dew point
|
||||
alpha = MAGNUS_K2 * self._indoor_temp / (MAGNUS_K3 + self._indoor_temp)
|
||||
beta = MAGNUS_K2 * MAGNUS_K3 / (MAGNUS_K3 + self._indoor_temp)
|
||||
|
||||
if self._indoor_hum == 0:
|
||||
self._dewpoint = -50 # not defined, assume very low value
|
||||
else:
|
||||
self._dewpoint = \
|
||||
MAGNUS_K3 * (alpha + math.log(self._indoor_hum / 100.0)) / \
|
||||
(beta - math.log(self._indoor_hum / 100.0))
|
||||
_LOGGER.debug("Dewpoint: %f " + TEMP_CELSIUS, self._dewpoint)
|
||||
|
||||
def _calc_moldindicator(self):
|
||||
"""Calculate the humidity at the (cold) calibration point."""
|
||||
if None in (self._dewpoint, self._calib_factor) or \
|
||||
self._calib_factor == 0:
|
||||
|
||||
_LOGGER.debug("Invalid inputs - dewpoint: %s,"
|
||||
" calibration-factor: %s",
|
||||
self._dewpoint, self._calib_factor)
|
||||
self._state = None
|
||||
return
|
||||
|
||||
# first calculate the approximate temperature at the calibration point
|
||||
self._crit_temp = \
|
||||
self._outdoor_temp + (self._indoor_temp - self._outdoor_temp) / \
|
||||
self._calib_factor
|
||||
|
||||
_LOGGER.debug(
|
||||
"Estimated Critical Temperature: %f " +
|
||||
TEMP_CELSIUS, self._crit_temp)
|
||||
|
||||
# Then calculate the humidity at this point
|
||||
alpha = MAGNUS_K2 * self._crit_temp / (MAGNUS_K3 + self._crit_temp)
|
||||
beta = MAGNUS_K2 * MAGNUS_K3 / (MAGNUS_K3 + self._crit_temp)
|
||||
|
||||
crit_humidity = \
|
||||
math.exp(
|
||||
(self._dewpoint * beta - MAGNUS_K3 * alpha) /
|
||||
(self._dewpoint + MAGNUS_K3)) * 100.0
|
||||
|
||||
# check bounds and format
|
||||
if crit_humidity > 100:
|
||||
self._state = '100'
|
||||
elif crit_humidity < 0:
|
||||
self._state = '0'
|
||||
else:
|
||||
self._state = '{0:d}'.format(int(crit_humidity))
|
||||
|
||||
_LOGGER.debug('Mold indicator humidity: %s ', self._state)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return "%"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._is_metric:
|
||||
return {
|
||||
ATTR_DEWPOINT: self._dewpoint,
|
||||
ATTR_CRITICAL_TEMP: self._crit_temp,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
ATTR_DEWPOINT:
|
||||
util.temperature.celcius_to_fahrenheit(
|
||||
self._dewpoint),
|
||||
ATTR_CRITICAL_TEMP:
|
||||
util.temperature.celcius_to_fahrenheit(
|
||||
self._crit_temp),
|
||||
}
|
@ -4,6 +4,8 @@ Support for Nest Thermostat Sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.nest/
|
||||
"""
|
||||
from itertools import chain
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.nest as nest
|
||||
@ -29,9 +31,13 @@ WEATHER_VARS = {'weather_humidity': 'humidity',
|
||||
SENSOR_UNITS = {'humidity': '%', 'battery_level': 'V',
|
||||
'kph': 'kph', 'temperature': '°C'}
|
||||
|
||||
PROTECT_VARS = ['co_status',
|
||||
'smoke_status',
|
||||
'battery_level']
|
||||
|
||||
SENSOR_TEMP_TYPES = ['temperature', 'target']
|
||||
|
||||
_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + \
|
||||
_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS + \
|
||||
list(WEATHER_VARS.keys())
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
@ -44,20 +50,34 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Nest Sensor."""
|
||||
for structure, device in nest.devices():
|
||||
for structure, device in chain(nest.devices(), nest.protect_devices()):
|
||||
sensors = [NestBasicSensor(structure, device, variable)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]
|
||||
if variable in SENSOR_TYPES]
|
||||
if variable in SENSOR_TYPES and is_thermostat(device)]
|
||||
sensors += [NestTempSensor(structure, device, variable)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]
|
||||
if variable in SENSOR_TEMP_TYPES]
|
||||
if variable in SENSOR_TEMP_TYPES and is_thermostat(device)]
|
||||
sensors += [NestWeatherSensor(structure, device,
|
||||
WEATHER_VARS[variable])
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]
|
||||
if variable in WEATHER_VARS]
|
||||
if variable in WEATHER_VARS and is_thermostat(device)]
|
||||
sensors += [NestProtectSensor(structure, device, variable)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]
|
||||
if variable in PROTECT_VARS and is_protect(device)]
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
def is_thermostat(device):
|
||||
"""Target devices that are Nest Thermostats."""
|
||||
return bool(device.__class__.__name__ == 'Device')
|
||||
|
||||
|
||||
def is_protect(device):
|
||||
"""Target devices that are Nest Protect Smoke Alarms."""
|
||||
return bool(device.__class__.__name__ == 'ProtectDevice')
|
||||
|
||||
|
||||
class NestSensor(Entity):
|
||||
"""Representation of a Nest sensor."""
|
||||
|
||||
@ -130,3 +150,28 @@ class NestWeatherSensor(NestSensor):
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return SENSOR_UNITS.get(self.variable, None)
|
||||
|
||||
|
||||
class NestProtectSensor(NestSensor):
|
||||
"""Return the state of nest protect."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
state = getattr(self.device, self.variable)
|
||||
if self.variable == 'battery_level':
|
||||
return getattr(self.device, self.variable)
|
||||
else:
|
||||
if state == 0:
|
||||
return 'Ok'
|
||||
if state == 1 or state == 2:
|
||||
return 'Warning'
|
||||
if state == 3:
|
||||
return 'Emergency'
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return "{} {}".format(self.device.where.capitalize(), self.variable)
|
||||
|
@ -7,11 +7,12 @@ https://home-assistant.io/components/sensor.speedtest/
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from subprocess import check_output
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
|
||||
REQUIREMENTS = ['speedtest-cli==0.3.4']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -21,6 +22,7 @@ _SPEEDTEST_REGEX = re.compile(r'Ping:\s(\d+\.\d+)\sms[\r\n]+'
|
||||
r'Upload:\s(\d+\.\d+)\sMbit/s[\r\n]+')
|
||||
|
||||
CONF_MONITORED_CONDITIONS = 'monitored_conditions'
|
||||
CONF_SECOND = 'second'
|
||||
CONF_MINUTE = 'minute'
|
||||
CONF_HOUR = 'hour'
|
||||
CONF_DAY = 'day'
|
||||
@ -30,13 +32,10 @@ SENSOR_TYPES = {
|
||||
'upload': ['Upload', 'Mbit/s'],
|
||||
}
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Speedtest sensor."""
|
||||
data = SpeedtestData()
|
||||
data = SpeedtestData(hass, config)
|
||||
dev = []
|
||||
for sensor in config[CONF_MONITORED_CONDITIONS]:
|
||||
if sensor not in SENSOR_TYPES:
|
||||
@ -46,6 +45,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
def update(call=None):
|
||||
"""Update service for manual updates."""
|
||||
data.update(dt_util.now())
|
||||
for sensor in dev:
|
||||
sensor.update()
|
||||
|
||||
hass.services.register(DOMAIN, 'update_speedtest', update)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SpeedtestSensor(Entity):
|
||||
@ -76,7 +83,6 @@ class SpeedtestSensor(Entity):
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
self.speedtest_client.update()
|
||||
data = self.speedtest_client.data
|
||||
if data is None:
|
||||
return
|
||||
@ -92,12 +98,16 @@ class SpeedtestSensor(Entity):
|
||||
class SpeedtestData(object):
|
||||
"""Get the latest data from speedtest.net."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the data object."""
|
||||
self.data = None
|
||||
track_time_change(hass, self.update,
|
||||
second=config.get(CONF_SECOND, 0),
|
||||
minute=config.get(CONF_MINUTE, 0),
|
||||
hour=config.get(CONF_HOUR, None),
|
||||
day=config.get(CONF_DAY, None))
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
def update(self, now):
|
||||
"""Get the latest data from speedtest.net."""
|
||||
import speedtest_cli
|
||||
|
||||
|
61
homeassistant/components/sensor/supervisord.py
Normal file
61
homeassistant/components/sensor/supervisord.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""
|
||||
Sensor for Supervisord process status.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.supervisord/
|
||||
"""
|
||||
import logging
|
||||
import xmlrpc.client
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Supervisord platform."""
|
||||
try:
|
||||
supervisor_server = xmlrpc.client.ServerProxy(
|
||||
config.get('url', 'http://localhost:9001/RPC2'))
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.error('Could not connect to Supervisord')
|
||||
return
|
||||
processes = supervisor_server.supervisor.getAllProcessInfo()
|
||||
add_devices(
|
||||
[SupervisorProcessSensor(info, supervisor_server)
|
||||
for info in processes])
|
||||
|
||||
|
||||
class SupervisorProcessSensor(Entity):
|
||||
"""Represent a supervisor-monitored process."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
def __init__(self, info, server):
|
||||
"""Initialize the sensor."""
|
||||
self._info = info
|
||||
self._server = server
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._info.get('name')
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._info.get('statename')
|
||||
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
self._info = self._server.supervisor.getProcessInfo(
|
||||
self._info.get('name'))
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'group': self._info.get('group'),
|
||||
'description': self._info.get('description')
|
||||
}
|
6
homeassistant/components/sensor/systemmonitor.py
Normal file → Executable file
6
homeassistant/components/sensor/systemmonitor.py
Normal file → Executable file
@ -10,7 +10,7 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['psutil==4.1.0']
|
||||
REQUIREMENTS = ['psutil==4.2.0']
|
||||
SENSOR_TYPES = {
|
||||
'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'],
|
||||
'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'],
|
||||
@ -24,9 +24,9 @@ SENSOR_TYPES = {
|
||||
'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'],
|
||||
'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'],
|
||||
'network_out': ['Sent', 'MiB', 'mdi:server-network'],
|
||||
'network_in': ['Recieved', 'MiB', 'mdi:server-network'],
|
||||
'network_in': ['Received', 'MiB', 'mdi:server-network'],
|
||||
'packets_out': ['Packets sent', '', 'mdi:server-network'],
|
||||
'packets_in': ['Packets recieved', '', 'mdi:server-network'],
|
||||
'packets_in': ['Packets received', '', 'mdi:server-network'],
|
||||
'ipv4_address': ['IPv4 address', '', 'mdi:server-network'],
|
||||
'ipv6_address': ['IPv6 address', '', 'mdi:server-network'],
|
||||
'last_boot': ['Last Boot', '', 'mdi:clock'],
|
||||
|
34
homeassistant/components/switch/qwikswitch.py
Normal file
34
homeassistant/components/switch/qwikswitch.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""
|
||||
Support for Qwikswitch relays.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.qwikswitch/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.qwikswitch as qwikswitch
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
|
||||
DEPENDENCIES = ['qwikswitch']
|
||||
|
||||
|
||||
class QSSwitch(qwikswitch.QSToggleEntity, SwitchDevice):
|
||||
"""Switch based on a Qwikswitch relay module."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Store add_devices for the switch 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'] == 'switch':
|
||||
dev = QSSwitch(item, qsusb)
|
||||
add_devices([dev])
|
||||
qsusb.ha_objects[item['id']] = dev
|
@ -14,7 +14,7 @@ REQUIREMENTS = ['rpi-rf==0.9.5']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=unused-argument, import-error
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Find and return switches controlled by a generic RF device via GPIO."""
|
||||
import rpi_rf
|
||||
|
@ -40,7 +40,6 @@ class Thermostat(ThermostatDevice):
|
||||
self.thermostat = self.data.ecobee.get_thermostat(
|
||||
self.thermostat_index)
|
||||
self._name = self.thermostat['name']
|
||||
self._away = 'away' in self.thermostat['program']['currentClimateRef']
|
||||
self.hold_temp = hold_temp
|
||||
|
||||
def update(self):
|
||||
@ -121,9 +120,7 @@ class Thermostat(ThermostatDevice):
|
||||
@property
|
||||
def mode(self):
|
||||
"""Return current mode ie. home, away, sleep."""
|
||||
mode = self.thermostat['program']['currentClimateRef']
|
||||
self._away = 'away' in mode
|
||||
return mode
|
||||
return self.thermostat['program']['currentClimateRef']
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
@ -144,11 +141,16 @@ class Thermostat(ThermostatDevice):
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
mode = self.mode
|
||||
events = self.thermostat['events']
|
||||
for event in events:
|
||||
if event['running']:
|
||||
mode = event['holdClimateRef']
|
||||
break
|
||||
return 'away' in mode
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self._away = True
|
||||
if self.hold_temp:
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", "indefinite")
|
||||
@ -157,7 +159,6 @@ class Thermostat(ThermostatDevice):
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self._away = False
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
@ -180,20 +181,16 @@ class Thermostat(ThermostatDevice):
|
||||
|
||||
# def turn_home_mode_on(self):
|
||||
# """ Turns home mode on. """
|
||||
# self._away = False
|
||||
# self.data.ecobee.set_climate_hold(self.thermostat_index, "home")
|
||||
|
||||
# def turn_home_mode_off(self):
|
||||
# """ Turns home mode off. """
|
||||
# self._away = False
|
||||
# self.data.ecobee.resume_program(self.thermostat_index)
|
||||
|
||||
# def turn_sleep_mode_on(self):
|
||||
# """ Turns sleep mode on. """
|
||||
# self._away = False
|
||||
# self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep")
|
||||
|
||||
# def turn_sleep_mode_off(self):
|
||||
# """ Turns sleep mode off. """
|
||||
# self._away = False
|
||||
# self.data.ecobee.resume_program(self.thermostat_index)
|
||||
|
@ -1,7 +1,7 @@
|
||||
# coding: utf-8
|
||||
"""Constants used by Home Assistant components."""
|
||||
|
||||
__version__ = "0.19.4"
|
||||
__version__ = "0.20.0.dev0"
|
||||
REQUIRED_PYTHON_VER = (3, 4)
|
||||
|
||||
PLATFORM_FORMAT = '{}.{}'
|
||||
|
@ -79,6 +79,7 @@ class HomeAssistant(object):
|
||||
|
||||
def restart_homeassistant(*args):
|
||||
"""Reset Home Assistant."""
|
||||
_LOGGER.warning('Home Assistant requested a restart.')
|
||||
request_restart.set()
|
||||
request_shutdown.set()
|
||||
|
||||
@ -92,14 +93,21 @@ class HomeAssistant(object):
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
'Could not bind to SIGTERM. Are you running in a thread?')
|
||||
|
||||
while not request_shutdown.isSet():
|
||||
try:
|
||||
try:
|
||||
signal.signal(signal.SIGHUP, restart_homeassistant)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
'Could not bind to SIGHUP. Are you running in a thread?')
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
while not request_shutdown.isSet():
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.stop()
|
||||
|
||||
self.stop()
|
||||
return RESTART_EXIT_CODE if request_restart.isSet() else 0
|
||||
|
||||
def stop(self):
|
||||
|
@ -55,12 +55,23 @@ class EntityComponent(object):
|
||||
self._setup_platform(p_type, p_config)
|
||||
|
||||
if self.discovery_platforms:
|
||||
# Discovery listener for all items in discovery_platforms array
|
||||
# passed from a component's setup method (e.g. light/__init__.py)
|
||||
discovery.listen(
|
||||
self.hass, self.discovery_platforms.keys(),
|
||||
lambda service, info:
|
||||
self._setup_platform(self.discovery_platforms[service], {},
|
||||
info))
|
||||
|
||||
# Generic discovery listener for loading platform dynamically
|
||||
# Refer to: homeassistant.components.discovery.load_platform()
|
||||
def load_platform_callback(service, info):
|
||||
"""Callback to load a platform."""
|
||||
platform = info.pop(discovery.LOAD_PLATFORM)
|
||||
self._setup_platform(platform, {}, info if info else None)
|
||||
discovery.listen(self.hass, discovery.LOAD_PLATFORM + '.' +
|
||||
self.domain, load_platform_callback)
|
||||
|
||||
def extract_from_service(self, service):
|
||||
"""Extract all known entities from a service call.
|
||||
|
||||
|
@ -16,8 +16,8 @@ from homeassistant.components.thermostat import (
|
||||
ATTR_AWAY_MODE, ATTR_FAN, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_TEMPERATURE)
|
||||
from homeassistant.components.hvac import (
|
||||
ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION, ATTR_AUX_HEAT,
|
||||
SERVICE_SET_HUMIDITY, SERVICE_SET_SWING,
|
||||
ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION_MODE, ATTR_AUX_HEAT,
|
||||
SERVICE_SET_HUMIDITY, SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_OPERATION_MODE, SERVICE_SET_AUX_HEAT)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY,
|
||||
@ -48,8 +48,8 @@ SERVICE_ATTRIBUTES = {
|
||||
SERVICE_SET_FAN_MODE: [ATTR_FAN],
|
||||
SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE],
|
||||
SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY],
|
||||
SERVICE_SET_SWING: [ATTR_SWING_MODE],
|
||||
SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION],
|
||||
SERVICE_SET_SWING_MODE: [ATTR_SWING_MODE],
|
||||
SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE],
|
||||
SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT],
|
||||
SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE],
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ def render(hass, template, variables=None, **kwargs):
|
||||
'states': AllStates(hass),
|
||||
'utcnow': utcnow,
|
||||
'as_timestamp': dt_util.as_timestamp,
|
||||
'relative_time': dt_util.get_age
|
||||
}).render(kwargs).strip()
|
||||
except jinja2.TemplateError as err:
|
||||
raise TemplateError(err)
|
||||
|
@ -1,48 +1,47 @@
|
||||
"""Color util methods."""
|
||||
import math
|
||||
# pylint: disable=unused-import
|
||||
from webcolors import html5_parse_legacy_color as color_name_to_rgb # noqa
|
||||
|
||||
HASS_COLOR_MAX = 500 # mireds (inverted)
|
||||
HASS_COLOR_MIN = 154
|
||||
|
||||
|
||||
# Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py
|
||||
# Taken from:
|
||||
# http://www.developers.meethue.com/documentation/color-conversions-rgb-xy
|
||||
# License: Code is given as is. Use at your own risk and discretion.
|
||||
# pylint: disable=invalid-name
|
||||
def color_RGB_to_xy(R, G, B):
|
||||
"""Convert from RGB color to XY color."""
|
||||
if R + G + B == 0:
|
||||
return 0, 0
|
||||
return 0, 0, 0
|
||||
|
||||
var_R = (R / 255.)
|
||||
var_G = (G / 255.)
|
||||
var_B = (B / 255.)
|
||||
R = R / 255
|
||||
B = B / 255
|
||||
G = G / 255
|
||||
|
||||
if var_R > 0.04045:
|
||||
var_R = ((var_R + 0.055) / 1.055) ** 2.4
|
||||
else:
|
||||
var_R /= 12.92
|
||||
# Gamma correction
|
||||
R = pow((R + 0.055) / (1.0 + 0.055),
|
||||
2.4) if (R > 0.04045) else (R / 12.92)
|
||||
G = pow((G + 0.055) / (1.0 + 0.055),
|
||||
2.4) if (G > 0.04045) else (G / 12.92)
|
||||
B = pow((B + 0.055) / (1.0 + 0.055),
|
||||
2.4) if (B > 0.04045) else (B / 12.92)
|
||||
|
||||
if var_G > 0.04045:
|
||||
var_G = ((var_G + 0.055) / 1.055) ** 2.4
|
||||
else:
|
||||
var_G /= 12.92
|
||||
# Wide RGB D65 conversion formula
|
||||
X = R * 0.664511 + G * 0.154324 + B * 0.162028
|
||||
Y = R * 0.313881 + G * 0.668433 + B * 0.047685
|
||||
Z = R * 0.000088 + G * 0.072310 + B * 0.986039
|
||||
|
||||
if var_B > 0.04045:
|
||||
var_B = ((var_B + 0.055) / 1.055) ** 2.4
|
||||
else:
|
||||
var_B /= 12.92
|
||||
# Convert XYZ to xy
|
||||
x = X / (X + Y + Z)
|
||||
y = Y / (X + Y + Z)
|
||||
|
||||
var_R *= 100
|
||||
var_G *= 100
|
||||
var_B *= 100
|
||||
# Brightness
|
||||
Y = 1 if Y > 1 else Y
|
||||
brightness = round(Y * 255)
|
||||
|
||||
# Observer. = 2 deg, Illuminant = D65
|
||||
X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
|
||||
Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
|
||||
Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
|
||||
|
||||
# Convert XYZ to xy, see CIE 1931 color space on wikipedia
|
||||
return X / (X + Y + Z), Y / (X + Y + Z)
|
||||
return round(x, 3), round(y, 3), brightness
|
||||
|
||||
|
||||
# taken from
|
||||
|
@ -152,3 +152,52 @@ def parse_time(time_str):
|
||||
except ValueError:
|
||||
# ValueError if value cannot be converted to an int or not in range
|
||||
return None
|
||||
|
||||
|
||||
# Found in this gist: https://gist.github.com/zhangsen/1199964
|
||||
def get_age(date):
|
||||
# pylint: disable=too-many-return-statements
|
||||
"""
|
||||
Take a datetime and return its "age" as a string.
|
||||
|
||||
The age can be in second, minute, hour, day, month or year. Only the
|
||||
biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will
|
||||
be returned.
|
||||
Make sure date is not in the future, or else it won't work.
|
||||
"""
|
||||
def formatn(number, unit):
|
||||
"""Add "unit" if it's plural."""
|
||||
if number == 1:
|
||||
return "1 %s" % unit
|
||||
elif number > 1:
|
||||
return "%d %ss" % (number, unit)
|
||||
|
||||
def q_n_r(first, second):
|
||||
"""Return quotient and remaining."""
|
||||
return first // second, first % second
|
||||
|
||||
delta = now() - date
|
||||
day = delta.days
|
||||
second = delta.seconds
|
||||
|
||||
year, day = q_n_r(day, 365)
|
||||
if year > 0:
|
||||
return formatn(year, 'year')
|
||||
|
||||
month, day = q_n_r(day, 30)
|
||||
if month > 0:
|
||||
return formatn(month, 'month')
|
||||
if day > 0:
|
||||
return formatn(day, 'day')
|
||||
|
||||
hour, second = q_n_r(second, 3600)
|
||||
if hour > 0:
|
||||
return formatn(hour, 'hour')
|
||||
|
||||
minute, second = q_n_r(second, 60)
|
||||
if minute > 0:
|
||||
return formatn(minute, 'minute')
|
||||
if second > 0:
|
||||
return formatn(second, 'second')
|
||||
|
||||
return "0 second"
|
||||
|
@ -3,6 +3,7 @@ import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
import glob
|
||||
import yaml
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@ -44,6 +45,44 @@ def _include_yaml(loader, node):
|
||||
return load_yaml(fname)
|
||||
|
||||
|
||||
def _include_dir_named_yaml(loader, node):
|
||||
"""Load multiple files from dir as a dict."""
|
||||
mapping = OrderedDict()
|
||||
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
|
||||
for fname in glob.glob(files):
|
||||
filename = os.path.splitext(os.path.basename(fname))[0]
|
||||
mapping[filename] = load_yaml(fname)
|
||||
return mapping
|
||||
|
||||
|
||||
def _include_dir_merge_named_yaml(loader, node):
|
||||
"""Load multiple files from dir as a merged dict."""
|
||||
mapping = OrderedDict()
|
||||
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
|
||||
for fname in glob.glob(files):
|
||||
loaded_yaml = load_yaml(fname)
|
||||
if isinstance(loaded_yaml, dict):
|
||||
mapping.update(loaded_yaml)
|
||||
return mapping
|
||||
|
||||
|
||||
def _include_dir_list_yaml(loader, node):
|
||||
"""Load multiple files from dir as a list."""
|
||||
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
|
||||
return [load_yaml(f) for f in glob.glob(files)]
|
||||
|
||||
|
||||
def _include_dir_merge_list_yaml(loader, node):
|
||||
"""Load multiple files from dir as a merged list."""
|
||||
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
|
||||
merged_list = []
|
||||
for fname in glob.glob(files):
|
||||
loaded_yaml = load_yaml(fname)
|
||||
if isinstance(loaded_yaml, list):
|
||||
merged_list.extend(loaded_yaml)
|
||||
return merged_list
|
||||
|
||||
|
||||
def _ordered_dict(loader, node):
|
||||
"""Load YAML mappings into an ordered dict to preserve key order."""
|
||||
loader.flatten_mapping(node)
|
||||
@ -84,3 +123,9 @@ yaml.SafeLoader.add_constructor('!include', _include_yaml)
|
||||
yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
||||
_ordered_dict)
|
||||
yaml.SafeLoader.add_constructor('!env_var', _env_var_yaml)
|
||||
yaml.SafeLoader.add_constructor('!include_dir_list', _include_dir_list_yaml)
|
||||
yaml.SafeLoader.add_constructor('!include_dir_merge_list',
|
||||
_include_dir_merge_list_yaml)
|
||||
yaml.SafeLoader.add_constructor('!include_dir_named', _include_dir_named_yaml)
|
||||
yaml.SafeLoader.add_constructor('!include_dir_merge_named',
|
||||
_include_dir_merge_named_yaml)
|
||||
|
@ -6,6 +6,7 @@ pip>=7.0.0
|
||||
vincenty==0.1.4
|
||||
jinja2>=2.8
|
||||
voluptuous==0.8.9
|
||||
webcolors==1.5
|
||||
|
||||
# homeassistant.components.isy994
|
||||
PyISY==1.0.5
|
||||
@ -37,6 +38,11 @@ blockchain==1.3.1
|
||||
# homeassistant.components.thermostat.eq3btsmart
|
||||
# bluepy_devices>=0.2.0
|
||||
|
||||
# homeassistant.components.notify.aws_lambda
|
||||
# homeassistant.components.notify.aws_sns
|
||||
# homeassistant.components.notify.aws_sqs
|
||||
boto3==1.3.1
|
||||
|
||||
# homeassistant.components.notify.xmpp
|
||||
dnspython3==1.12.0
|
||||
|
||||
@ -97,6 +103,9 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl
|
||||
# homeassistant.components.alarm_control_panel.alarmdotcom
|
||||
https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1
|
||||
|
||||
# homeassistant.components.media_player.roku
|
||||
https://github.com/bah2830/python-roku/archive/3.1.1.zip#python-roku==3.1.1
|
||||
|
||||
# homeassistant.components.modbus
|
||||
https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0
|
||||
|
||||
@ -109,8 +118,11 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9.
|
||||
# homeassistant.components.sensor.sabnzbd
|
||||
https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
|
||||
|
||||
# homeassistant.components.qwikswitch
|
||||
https://github.com/kellerza/pyqwikswitch/archive/v0.3.zip#pyqwikswitch==0.3
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
https://github.com/nkgilley/python-ecobee-api/archive/92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4
|
||||
https://github.com/nkgilley/python-ecobee-api/archive/4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5
|
||||
|
||||
# homeassistant.components.switch.edimax
|
||||
https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1
|
||||
@ -119,7 +131,7 @@ https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f
|
||||
https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3
|
||||
|
||||
# homeassistant.components.sensor.gtfs
|
||||
https://github.com/robbiet480/pygtfs/archive/432414b720c580fb2667a0a48f539118a2d95969.zip#pygtfs==0.1.2
|
||||
https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3
|
||||
|
||||
# homeassistant.components.scene.hunterdouglas_powerview
|
||||
https://github.com/sander76/powerviewApi/archive/master.zip#powerviewApi==0.2
|
||||
@ -130,6 +142,9 @@ https://github.com/theolind/pymysensors/archive/cc5d0b325e13c2b623fa934f69eea7cd
|
||||
# homeassistant.components.notify.googlevoice
|
||||
https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#pygooglevoice-sms==0.0.1
|
||||
|
||||
# homeassistant.components.media_player.lg_netcast
|
||||
https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0
|
||||
|
||||
# homeassistant.components.influxdb
|
||||
influxdb==2.12.0
|
||||
|
||||
@ -153,7 +168,7 @@ messagebird==1.2.0
|
||||
mficlient==0.3.0
|
||||
|
||||
# homeassistant.components.discovery
|
||||
netdisco==0.6.6
|
||||
netdisco==0.6.7
|
||||
|
||||
# homeassistant.components.sensor.neurio_energy
|
||||
neurio==0.2.10
|
||||
@ -168,6 +183,7 @@ paho-mqtt==1.1
|
||||
panasonic_viera==0.2
|
||||
|
||||
# homeassistant.components.device_tracker.aruba
|
||||
# homeassistant.components.device_tracker.asuswrt
|
||||
pexpect==4.0.1
|
||||
|
||||
# homeassistant.components.light.hue
|
||||
@ -180,7 +196,7 @@ plexapi==1.1.0
|
||||
proliphix==0.1.0
|
||||
|
||||
# homeassistant.components.sensor.systemmonitor
|
||||
psutil==4.1.0
|
||||
psutil==4.2.0
|
||||
|
||||
# homeassistant.components.notify.pushbullet
|
||||
pushbullet.py==0.10.0
|
||||
@ -215,8 +231,11 @@ pyfttt==0.3
|
||||
# homeassistant.components.device_tracker.icloud
|
||||
pyicloud==0.8.3
|
||||
|
||||
# homeassistant.components.sensor.lastfm
|
||||
pylast==1.6.0
|
||||
|
||||
# homeassistant.components.sensor.loopenergy
|
||||
pyloopenergy==0.0.10
|
||||
pyloopenergy==0.0.12
|
||||
|
||||
# homeassistant.components.device_tracker.netgear
|
||||
pynetgear==0.3.3
|
||||
@ -241,7 +260,7 @@ python-forecastio==1.3.4
|
||||
python-mpd2==0.5.5
|
||||
|
||||
# homeassistant.components.nest
|
||||
python-nest==2.6.0
|
||||
python-nest==2.9.2
|
||||
|
||||
# homeassistant.components.device_tracker.nmap_tracker
|
||||
python-nmap==0.6.0
|
||||
@ -253,7 +272,7 @@ python-pushover==0.2
|
||||
python-statsd==1.7.2
|
||||
|
||||
# homeassistant.components.notify.telegram
|
||||
python-telegram-bot==4.0.1
|
||||
python-telegram-bot==4.1.1
|
||||
|
||||
# homeassistant.components.sensor.twitch
|
||||
python-twitch==1.2.0
|
||||
@ -280,7 +299,7 @@ pywemo==0.4.2
|
||||
radiotherm==1.2
|
||||
|
||||
# homeassistant.components.switch.rpi_rf
|
||||
rpi-rf==0.9.5
|
||||
# rpi-rf==0.9.5
|
||||
|
||||
# homeassistant.components.media_player.yamaha
|
||||
rxv==0.1.11
|
||||
@ -326,6 +345,9 @@ tellive-py==0.5.2
|
||||
# homeassistant.components.switch.transmission
|
||||
transmissionrpc==0.11
|
||||
|
||||
# homeassistant.components.notify.twilio_sms
|
||||
twilio==5.4.0
|
||||
|
||||
# homeassistant.components.sensor.uber
|
||||
uber_rides==0.2.1
|
||||
|
||||
@ -344,6 +366,9 @@ vsure==0.8.1
|
||||
# homeassistant.components.switch.wake_on_lan
|
||||
wakeonlan==0.2.2
|
||||
|
||||
# homeassistant.components.media_player.gpmdp
|
||||
websocket-client==0.35.0
|
||||
|
||||
# homeassistant.components.zigbee
|
||||
xbee-helper==0.0.7
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
flake8>=2.5.1
|
||||
flake8>=2.5.4
|
||||
pylint>=1.5.5
|
||||
coveralls>=1.1
|
||||
pytest>=2.9.1
|
||||
|
@ -15,7 +15,7 @@ if [ -d python-openzwave ]; then
|
||||
git pull --recurse-submodules=yes
|
||||
git submodule update --init --recursive
|
||||
else
|
||||
git clone --recursive https://github.com/OpenZWave/python-openzwave.git
|
||||
git clone --recursive --depth 1 https://github.com/OpenZWave/python-openzwave.git
|
||||
cd python-openzwave
|
||||
fi
|
||||
|
||||
|
@ -8,6 +8,7 @@ import sys
|
||||
|
||||
COMMENT_REQUIREMENTS = [
|
||||
'RPi.GPIO',
|
||||
'rpi-rf',
|
||||
'Adafruit_Python_DHT',
|
||||
'fritzconnection',
|
||||
'pybluez',
|
||||
|
@ -12,8 +12,9 @@ User=%i
|
||||
# Enable the following line if you get network-related HA errors during boot
|
||||
#ExecStartPre=/usr/bin/sleep 60
|
||||
# Use `whereis hass` to determine the path of hass
|
||||
ExecStart=/usr/bin/hass
|
||||
ExecStart=/usr/bin/hass --runner
|
||||
SendSIGKILL=no
|
||||
RestartForceExitStatus=100
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
1
setup.py
1
setup.py
@ -18,6 +18,7 @@ REQUIRES = [
|
||||
'vincenty==0.1.4',
|
||||
'jinja2>=2.8',
|
||||
'voluptuous==0.8.9',
|
||||
'webcolors==1.5',
|
||||
]
|
||||
|
||||
setup(
|
||||
|
@ -55,6 +55,21 @@ LOCATION_MESSAGE_INACCURATE = {
|
||||
'tst': 1,
|
||||
'vel': 0}
|
||||
|
||||
LOCATION_MESSAGE_ZERO_ACCURACY = {
|
||||
'batt': 92,
|
||||
'cog': 248,
|
||||
'tid': 'user',
|
||||
'lon': 2.0,
|
||||
't': 'u',
|
||||
'alt': 27,
|
||||
'acc': 0,
|
||||
'p': 101.3977584838867,
|
||||
'vac': 4,
|
||||
'lat': 6.0,
|
||||
'_type': 'location',
|
||||
'tst': 1,
|
||||
'vel': 0}
|
||||
|
||||
REGION_ENTER_MESSAGE = {
|
||||
'lon': 1.0,
|
||||
'event': 'enter',
|
||||
@ -204,6 +219,14 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase):
|
||||
self.assert_location_latitude(2.0)
|
||||
self.assert_location_longitude(1.0)
|
||||
|
||||
def test_location_zero_accuracy_gps(self):
|
||||
"""Ignore the location for zero accuracy GPS information."""
|
||||
self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE)
|
||||
self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE_ZERO_ACCURACY)
|
||||
|
||||
self.assert_location_latitude(2.0)
|
||||
self.assert_location_longitude(1.0)
|
||||
|
||||
def test_event_entry_exit(self):
|
||||
"""Test the entry event."""
|
||||
self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)
|
||||
@ -230,6 +253,20 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase):
|
||||
# Left clean zone state
|
||||
self.assertFalse(owntracks.REGIONS_ENTERED[USER])
|
||||
|
||||
def test_event_with_spaces(self):
|
||||
"""Test the entry event."""
|
||||
message = REGION_ENTER_MESSAGE.copy()
|
||||
message['desc'] = "inner 2"
|
||||
self.send_message(EVENT_TOPIC, message)
|
||||
self.assert_location_state('inner_2')
|
||||
|
||||
message = REGION_LEAVE_MESSAGE.copy()
|
||||
message['desc'] = "inner 2"
|
||||
self.send_message(EVENT_TOPIC, message)
|
||||
|
||||
# Left clean zone state
|
||||
self.assertFalse(owntracks.REGIONS_ENTERED[USER])
|
||||
|
||||
def test_event_entry_exit_inaccurate(self):
|
||||
"""Test the event for inaccurate exit."""
|
||||
self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)
|
||||
|
@ -33,7 +33,7 @@ class TestDemoHvac(unittest.TestCase):
|
||||
self.assertEqual(21, state.attributes.get('temperature'))
|
||||
self.assertEqual('on', state.attributes.get('away_mode'))
|
||||
self.assertEqual(22, state.attributes.get('current_temperature'))
|
||||
self.assertEqual("On High", state.attributes.get('fan'))
|
||||
self.assertEqual("On High", state.attributes.get('fan_mode'))
|
||||
self.assertEqual(67, state.attributes.get('humidity'))
|
||||
self.assertEqual(54, state.attributes.get('current_humidity'))
|
||||
self.assertEqual("Off", state.attributes.get('swing_mode'))
|
||||
@ -81,17 +81,17 @@ class TestDemoHvac(unittest.TestCase):
|
||||
def test_set_fan_mode_bad_attr(self):
|
||||
"""Test setting fan mode without required attribute."""
|
||||
state = self.hass.states.get(ENTITY_HVAC)
|
||||
self.assertEqual("On High", state.attributes.get('fan'))
|
||||
self.assertEqual("On High", state.attributes.get('fan_mode'))
|
||||
hvac.set_fan_mode(self.hass, None, ENTITY_HVAC)
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual("On High", state.attributes.get('fan'))
|
||||
self.assertEqual("On High", state.attributes.get('fan_mode'))
|
||||
|
||||
def test_set_fan_mode(self):
|
||||
"""Test setting of new fan mode."""
|
||||
hvac.set_fan_mode(self.hass, "On Low", ENTITY_HVAC)
|
||||
self.hass.pool.block_till_done()
|
||||
state = self.hass.states.get(ENTITY_HVAC)
|
||||
self.assertEqual("On Low", state.attributes.get('fan'))
|
||||
self.assertEqual("On Low", state.attributes.get('fan_mode'))
|
||||
|
||||
def test_set_swing_mode_bad_attr(self):
|
||||
"""Test setting swing mode without required attribute."""
|
||||
|
131
tests/components/sensor/test_moldindicator.py
Normal file
131
tests/components/sensor/test_moldindicator.py
Normal file
@ -0,0 +1,131 @@
|
||||
"""The tests for the MoldIndicator sensor."""
|
||||
import unittest
|
||||
|
||||
import homeassistant.components.sensor as sensor
|
||||
from homeassistant.components.sensor.mold_indicator import (ATTR_DEWPOINT,
|
||||
ATTR_CRITICAL_TEMP)
|
||||
from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT,
|
||||
TEMP_CELSIUS)
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
|
||||
class TestSensorMoldIndicator(unittest.TestCase):
|
||||
"""Test the MoldIndicator sensor."""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.hass.states.set('test.indoortemp', '20',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.states.set('test.outdoortemp', '10',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.states.set('test.indoorhumidity', '50',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: '%'})
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop down everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_setup(self):
|
||||
"""Test the mold indicator sensor setup."""
|
||||
self.assertTrue(sensor.setup(self.hass, {
|
||||
'sensor': {
|
||||
'platform': 'mold_indicator',
|
||||
'indoor_temp_sensor': 'test.indoortemp',
|
||||
'outdoor_temp_sensor': 'test.outdoortemp',
|
||||
'indoor_humidity_sensor': 'test.indoorhumidity',
|
||||
'calibration_factor': '2.0'
|
||||
}
|
||||
}))
|
||||
|
||||
moldind = self.hass.states.get('sensor.mold_indicator')
|
||||
assert moldind
|
||||
assert '%' == moldind.attributes.get('unit_of_measurement')
|
||||
|
||||
def test_invalidhum(self):
|
||||
"""Test invalid sensor values."""
|
||||
self.hass.states.set('test.indoortemp', '10',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.states.set('test.outdoortemp', '10',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.states.set('test.indoorhumidity', '0',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: '%'})
|
||||
|
||||
self.assertTrue(sensor.setup(self.hass, {
|
||||
'sensor': {
|
||||
'platform': 'mold_indicator',
|
||||
'indoor_temp_sensor': 'test.indoortemp',
|
||||
'outdoor_temp_sensor': 'test.outdoortemp',
|
||||
'indoor_humidity_sensor': 'test.indoorhumidity',
|
||||
'calibration_factor': '2.0'
|
||||
}
|
||||
}))
|
||||
moldind = self.hass.states.get('sensor.mold_indicator')
|
||||
assert moldind
|
||||
|
||||
# assert state
|
||||
assert moldind.state == '0'
|
||||
|
||||
def test_calculation(self):
|
||||
"""Test the mold indicator internal calculations."""
|
||||
self.assertTrue(sensor.setup(self.hass, {
|
||||
'sensor': {
|
||||
'platform': 'mold_indicator',
|
||||
'indoor_temp_sensor': 'test.indoortemp',
|
||||
'outdoor_temp_sensor': 'test.outdoortemp',
|
||||
'indoor_humidity_sensor': 'test.indoorhumidity',
|
||||
'calibration_factor': '2.0'
|
||||
}
|
||||
}))
|
||||
|
||||
moldind = self.hass.states.get('sensor.mold_indicator')
|
||||
assert moldind
|
||||
|
||||
# assert dewpoint
|
||||
dewpoint = moldind.attributes.get(ATTR_DEWPOINT)
|
||||
assert dewpoint
|
||||
assert dewpoint > 9.25
|
||||
assert dewpoint < 9.26
|
||||
|
||||
# assert temperature estimation
|
||||
esttemp = moldind.attributes.get(ATTR_CRITICAL_TEMP)
|
||||
assert esttemp
|
||||
assert esttemp > 14.9
|
||||
assert esttemp < 15.1
|
||||
|
||||
# assert mold indicator value
|
||||
state = moldind.state
|
||||
assert state
|
||||
assert state == '68'
|
||||
|
||||
def test_sensor_changed(self):
|
||||
"""Test the sensor_changed function."""
|
||||
self.assertTrue(sensor.setup(self.hass, {
|
||||
'sensor': {
|
||||
'platform': 'mold_indicator',
|
||||
'indoor_temp_sensor': 'test.indoortemp',
|
||||
'outdoor_temp_sensor': 'test.outdoortemp',
|
||||
'indoor_humidity_sensor': 'test.indoorhumidity',
|
||||
'calibration_factor': '2.0'
|
||||
}
|
||||
}))
|
||||
|
||||
# Change indoor temp
|
||||
self.hass.states.set('test.indoortemp', '30',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.pool.block_till_done()
|
||||
assert self.hass.states.get('sensor.mold_indicator').state == '90'
|
||||
|
||||
# Change outdoor temp
|
||||
self.hass.states.set('test.outdoortemp', '25',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.pool.block_till_done()
|
||||
assert self.hass.states.get('sensor.mold_indicator').state == '57'
|
||||
|
||||
# Change humidity
|
||||
self.hass.states.set('test.indoorhumidity', '20',
|
||||
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||
self.hass.pool.block_till_done()
|
||||
assert self.hass.states.get('sensor.mold_indicator').state == '23'
|
88
tests/components/test_logentries.py
Normal file
88
tests/components/test_logentries.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""The tests for the Logentries component."""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import homeassistant.components.logentries as logentries
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED
|
||||
|
||||
|
||||
class TestLogentries(unittest.TestCase):
|
||||
"""Test the Logentries component."""
|
||||
|
||||
def test_setup_config_full(self):
|
||||
"""Test setup with all data."""
|
||||
config = {
|
||||
'logentries': {
|
||||
'host': 'host',
|
||||
'token': 'secret',
|
||||
}
|
||||
}
|
||||
hass = mock.MagicMock()
|
||||
self.assertTrue(logentries.setup(hass, config))
|
||||
self.assertTrue(hass.bus.listen.called)
|
||||
self.assertEqual(EVENT_STATE_CHANGED,
|
||||
hass.bus.listen.call_args_list[0][0][0])
|
||||
|
||||
def test_setup_config_defaults(self):
|
||||
"""Test setup with defaults."""
|
||||
config = {
|
||||
'logentries': {
|
||||
'host': 'host',
|
||||
'token': 'token',
|
||||
}
|
||||
}
|
||||
hass = mock.MagicMock()
|
||||
self.assertTrue(logentries.setup(hass, config))
|
||||
self.assertTrue(hass.bus.listen.called)
|
||||
self.assertEqual(EVENT_STATE_CHANGED,
|
||||
hass.bus.listen.call_args_list[0][0][0])
|
||||
|
||||
def _setup(self, mock_requests):
|
||||
"""Test the setup."""
|
||||
self.mock_post = mock_requests.post
|
||||
self.mock_request_exception = Exception
|
||||
mock_requests.exceptions.RequestException = self.mock_request_exception
|
||||
config = {
|
||||
'logentries': {
|
||||
'host': 'https://webhook.logentries.com/noformat/logs/token',
|
||||
'token': 'token'
|
||||
}
|
||||
}
|
||||
self.hass = mock.MagicMock()
|
||||
logentries.setup(self.hass, config)
|
||||
self.handler_method = self.hass.bus.listen.call_args_list[0][0][1]
|
||||
|
||||
@mock.patch.object(logentries, 'requests')
|
||||
@mock.patch('json.dumps')
|
||||
def test_event_listener(self, mock_dump, mock_requests):
|
||||
"""Test event listener."""
|
||||
mock_dump.side_effect = lambda x: x
|
||||
self._setup(mock_requests)
|
||||
|
||||
valid = {'1': 1,
|
||||
'1.0': 1.0,
|
||||
STATE_ON: 1,
|
||||
STATE_OFF: 0,
|
||||
'foo': 'foo'}
|
||||
for in_, out in valid.items():
|
||||
state = mock.MagicMock(state=in_,
|
||||
domain='fake',
|
||||
object_id='entity',
|
||||
attributes={})
|
||||
event = mock.MagicMock(data={'new_state': state},
|
||||
time_fired=12345)
|
||||
body = [{
|
||||
'domain': 'fake',
|
||||
'entity_id': 'entity',
|
||||
'attributes': {},
|
||||
'time': '12345',
|
||||
'value': out,
|
||||
}]
|
||||
payload = {'host': 'https://webhook.logentries.com/noformat/'
|
||||
'logs/token',
|
||||
'event': body}
|
||||
self.handler_method(event)
|
||||
self.mock_post.assert_called_once_with(
|
||||
payload['host'], data=payload, timeout=10)
|
||||
self.mock_post.reset_mock()
|
@ -1,6 +1,8 @@
|
||||
"""The tests for the Recorder component."""
|
||||
# pylint: disable=too-many-public-methods,protected-access
|
||||
import unittest
|
||||
import time
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.const import MATCH_ALL
|
||||
@ -10,7 +12,7 @@ from tests.common import get_test_home_assistant
|
||||
|
||||
|
||||
class TestRecorder(unittest.TestCase):
|
||||
"""Test the chromecast module."""
|
||||
"""Test the recorder module."""
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
"""Setup things to be run when tests are started."""
|
||||
@ -25,6 +27,52 @@ class TestRecorder(unittest.TestCase):
|
||||
self.hass.stop()
|
||||
recorder._INSTANCE.block_till_done()
|
||||
|
||||
def _add_test_states(self):
|
||||
"""Add multiple states to the db for testing."""
|
||||
now = int(time.time())
|
||||
five_days_ago = now - (60*60*24*5)
|
||||
attributes = {'test_attr': 5, 'test_attr_10': 'nice'}
|
||||
|
||||
self.hass.pool.block_till_done()
|
||||
recorder._INSTANCE.block_till_done()
|
||||
for event_id in range(5):
|
||||
if event_id < 3:
|
||||
timestamp = five_days_ago
|
||||
state = 'purgeme'
|
||||
else:
|
||||
timestamp = now
|
||||
state = 'dontpurgeme'
|
||||
recorder.query("INSERT INTO states ("
|
||||
"entity_id, domain, state, attributes,"
|
||||
"last_changed, last_updated, created,"
|
||||
"utc_offset, event_id)"
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
('test.recorder2', 'sensor', state,
|
||||
json.dumps(attributes), timestamp, timestamp,
|
||||
timestamp, -18000, event_id + 1000))
|
||||
|
||||
def _add_test_events(self):
|
||||
"""Add a few events for testing."""
|
||||
now = int(time.time())
|
||||
five_days_ago = now - (60*60*24*5)
|
||||
event_data = {'test_attr': 5, 'test_attr_10': 'nice'}
|
||||
|
||||
self.hass.pool.block_till_done()
|
||||
recorder._INSTANCE.block_till_done()
|
||||
for event_id in range(5):
|
||||
if event_id < 2:
|
||||
timestamp = five_days_ago
|
||||
event_type = 'EVENT_TEST_PURGE'
|
||||
else:
|
||||
timestamp = now
|
||||
event_type = 'EVENT_TEST'
|
||||
recorder.query("INSERT INTO events"
|
||||
"(event_type, event_data, origin, created,"
|
||||
"time_fired, utc_offset)"
|
||||
"VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(event_type, json.dumps(event_data), 'LOCAL',
|
||||
timestamp, timestamp, -18000))
|
||||
|
||||
def test_saving_state(self):
|
||||
"""Test saving and restoring a state."""
|
||||
entity_id = 'test.recorder'
|
||||
@ -76,3 +124,56 @@ class TestRecorder(unittest.TestCase):
|
||||
# Recorder uses SQLite and stores datetimes as integer unix timestamps
|
||||
assert event.time_fired.replace(microsecond=0) == \
|
||||
db_event.time_fired.replace(microsecond=0)
|
||||
|
||||
def test_purge_old_states(self):
|
||||
"""Test deleting old states."""
|
||||
self._add_test_states()
|
||||
# make sure we start with 5 states
|
||||
states = recorder.query_states('SELECT * FROM states')
|
||||
self.assertEqual(len(states), 5)
|
||||
|
||||
# run purge_old_data()
|
||||
recorder._INSTANCE.purge_days = 4
|
||||
recorder._INSTANCE._purge_old_data()
|
||||
|
||||
# we should only have 2 states left after purging
|
||||
states = recorder.query_states('SELECT * FROM states')
|
||||
self.assertEqual(len(states), 2)
|
||||
|
||||
def test_purge_old_events(self):
|
||||
"""Test deleting old events."""
|
||||
self._add_test_events()
|
||||
events = recorder.query_events('SELECT * FROM events WHERE '
|
||||
'event_type LIKE "EVENT_TEST%"')
|
||||
self.assertEqual(len(events), 5)
|
||||
|
||||
# run purge_old_data()
|
||||
recorder._INSTANCE.purge_days = 4
|
||||
recorder._INSTANCE._purge_old_data()
|
||||
|
||||
# now we should only have 3 events left
|
||||
events = recorder.query_events('SELECT * FROM events WHERE '
|
||||
'event_type LIKE "EVENT_TEST%"')
|
||||
self.assertEqual(len(events), 3)
|
||||
|
||||
def test_purge_disabled(self):
|
||||
"""Test leaving purge_days disabled."""
|
||||
self._add_test_states()
|
||||
self._add_test_events()
|
||||
# make sure we start with 5 states and events
|
||||
states = recorder.query_states('SELECT * FROM states')
|
||||
events = recorder.query_events('SELECT * FROM events WHERE '
|
||||
'event_type LIKE "EVENT_TEST%"')
|
||||
self.assertEqual(len(states), 5)
|
||||
self.assertEqual(len(events), 5)
|
||||
|
||||
# run purge_old_data()
|
||||
recorder._INSTANCE.purge_days = None
|
||||
recorder._INSTANCE._purge_old_data()
|
||||
|
||||
# we should have all of our states still
|
||||
states = recorder.query_states('SELECT * FROM states')
|
||||
events = recorder.query_events('SELECT * FROM events WHERE '
|
||||
'event_type LIKE "EVENT_TEST%"')
|
||||
self.assertEqual(len(states), 5)
|
||||
self.assertEqual(len(events), 5)
|
||||
|
@ -37,10 +37,10 @@ class TestRFXTRX(unittest.TestCase):
|
||||
'automatic_add': True,
|
||||
'devices': {}}}))
|
||||
|
||||
while len(rfxtrx.RFX_DEVICES) < 1:
|
||||
while len(rfxtrx.RFX_DEVICES) < 2:
|
||||
time.sleep(0.1)
|
||||
|
||||
self.assertEqual(len(rfxtrx.RFXOBJECT.sensors()), 1)
|
||||
self.assertEqual(len(rfxtrx.RFXOBJECT.sensors()), 2)
|
||||
|
||||
def test_valid_config(self):
|
||||
"""Test configuration."""
|
||||
|
@ -9,16 +9,17 @@ class TestColorUtil(unittest.TestCase):
|
||||
# pylint: disable=invalid-name
|
||||
def test_color_RGB_to_xy(self):
|
||||
"""Test color_RGB_to_xy."""
|
||||
self.assertEqual((0, 0), color_util.color_RGB_to_xy(0, 0, 0))
|
||||
self.assertEqual((0.3127159072215825, 0.3290014805066623),
|
||||
self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy(0, 0, 0))
|
||||
self.assertEqual((0.32, 0.336, 255),
|
||||
color_util.color_RGB_to_xy(255, 255, 255))
|
||||
|
||||
self.assertEqual((0.15001662234042554, 0.060006648936170214),
|
||||
self.assertEqual((0.136, 0.04, 12),
|
||||
color_util.color_RGB_to_xy(0, 0, 255))
|
||||
|
||||
self.assertEqual((0.3, 0.6), color_util.color_RGB_to_xy(0, 255, 0))
|
||||
self.assertEqual((0.172, 0.747, 170),
|
||||
color_util.color_RGB_to_xy(0, 255, 0))
|
||||
|
||||
self.assertEqual((0.6400744994567747, 0.3299705106316933),
|
||||
self.assertEqual((0.679, 0.321, 80),
|
||||
color_util.color_RGB_to_xy(255, 0, 0))
|
||||
|
||||
def test_color_xy_brightness_to_RGB(self):
|
||||
|
@ -133,3 +133,32 @@ class TestDateUtil(unittest.TestCase):
|
||||
def test_parse_datetime_returns_none_for_incorrect_format(self):
|
||||
"""Test parse_datetime returns None if incorrect format."""
|
||||
self.assertIsNone(dt_util.parse_datetime("not a datetime string"))
|
||||
|
||||
def test_get_age(self):
|
||||
"""Test get_age."""
|
||||
diff = dt_util.now() - timedelta(seconds=0)
|
||||
self.assertEqual(dt_util.get_age(diff), "0 second")
|
||||
|
||||
diff = dt_util.now() - timedelta(seconds=30)
|
||||
self.assertEqual(dt_util.get_age(diff), "30 seconds")
|
||||
|
||||
diff = dt_util.now() - timedelta(minutes=5)
|
||||
self.assertEqual(dt_util.get_age(diff), "5 minutes")
|
||||
|
||||
diff = dt_util.now() - timedelta(minutes=1)
|
||||
self.assertEqual(dt_util.get_age(diff), "1 minute")
|
||||
|
||||
diff = dt_util.now() - timedelta(minutes=300)
|
||||
self.assertEqual(dt_util.get_age(diff), "5 hours")
|
||||
|
||||
diff = dt_util.now() - timedelta(minutes=320)
|
||||
self.assertEqual(dt_util.get_age(diff), "5 hours")
|
||||
|
||||
diff = dt_util.now() - timedelta(minutes=2*60*24)
|
||||
self.assertEqual(dt_util.get_age(diff), "2 days")
|
||||
|
||||
diff = dt_util.now() - timedelta(minutes=32*60*24)
|
||||
self.assertEqual(dt_util.get_age(diff), "1 month")
|
||||
|
||||
diff = dt_util.now() - timedelta(minutes=365*60*24)
|
||||
self.assertEqual(dt_util.get_age(diff), "1 year")
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Test Home Assistant yaml loader."""
|
||||
import io
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from homeassistant.util import yaml
|
||||
|
||||
@ -32,3 +34,104 @@ class TestYaml(unittest.TestCase):
|
||||
pass
|
||||
else:
|
||||
assert 0
|
||||
|
||||
def test_enviroment_variable(self):
|
||||
"""Test config file with enviroment variable."""
|
||||
os.environ["PASSWORD"] = "secret_password"
|
||||
conf = "password: !env_var PASSWORD"
|
||||
with io.StringIO(conf) as f:
|
||||
doc = yaml.yaml.safe_load(f)
|
||||
assert doc['password'] == "secret_password"
|
||||
del os.environ["PASSWORD"]
|
||||
|
||||
def test_invalid_enviroment_variable(self):
|
||||
"""Test config file with no enviroment variable sat."""
|
||||
conf = "password: !env_var PASSWORD"
|
||||
try:
|
||||
with io.StringIO(conf) as f:
|
||||
yaml.yaml.safe_load(f)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
assert 0
|
||||
|
||||
def test_include_yaml(self):
|
||||
"""Test include yaml."""
|
||||
with tempfile.NamedTemporaryFile() as include_file:
|
||||
include_file.write(b"value")
|
||||
include_file.seek(0)
|
||||
conf = "key: !include {}".format(include_file.name)
|
||||
with io.StringIO(conf) as f:
|
||||
doc = yaml.yaml.safe_load(f)
|
||||
assert doc["key"] == "value"
|
||||
|
||||
def test_include_dir_list(self):
|
||||
"""Test include dir list yaml."""
|
||||
with tempfile.TemporaryDirectory() as include_dir:
|
||||
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
|
||||
suffix=".yaml", delete=False)
|
||||
file_1.write(b"one")
|
||||
file_1.close()
|
||||
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
|
||||
suffix=".yaml", delete=False)
|
||||
file_2.write(b"two")
|
||||
file_2.close()
|
||||
conf = "key: !include_dir_list {}".format(include_dir)
|
||||
with io.StringIO(conf) as f:
|
||||
doc = yaml.yaml.safe_load(f)
|
||||
assert sorted(doc["key"]) == sorted(["one", "two"])
|
||||
|
||||
def test_include_dir_named(self):
|
||||
"""Test include dir named yaml."""
|
||||
with tempfile.TemporaryDirectory() as include_dir:
|
||||
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
|
||||
suffix=".yaml", delete=False)
|
||||
file_1.write(b"one")
|
||||
file_1.close()
|
||||
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
|
||||
suffix=".yaml", delete=False)
|
||||
file_2.write(b"two")
|
||||
file_2.close()
|
||||
conf = "key: !include_dir_named {}".format(include_dir)
|
||||
correct = {}
|
||||
correct[os.path.splitext(os.path.basename(file_1.name))[0]] = "one"
|
||||
correct[os.path.splitext(os.path.basename(file_2.name))[0]] = "two"
|
||||
with io.StringIO(conf) as f:
|
||||
doc = yaml.yaml.safe_load(f)
|
||||
assert doc["key"] == correct
|
||||
|
||||
def test_include_dir_merge_list(self):
|
||||
"""Test include dir merge list yaml."""
|
||||
with tempfile.TemporaryDirectory() as include_dir:
|
||||
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
|
||||
suffix=".yaml", delete=False)
|
||||
file_1.write(b"- one")
|
||||
file_1.close()
|
||||
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
|
||||
suffix=".yaml", delete=False)
|
||||
file_2.write(b"- two\n- three")
|
||||
file_2.close()
|
||||
conf = "key: !include_dir_merge_list {}".format(include_dir)
|
||||
with io.StringIO(conf) as f:
|
||||
doc = yaml.yaml.safe_load(f)
|
||||
assert sorted(doc["key"]) == sorted(["one", "two", "three"])
|
||||
|
||||
def test_include_dir_merge_named(self):
|
||||
"""Test include dir merge named yaml."""
|
||||
with tempfile.TemporaryDirectory() as include_dir:
|
||||
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
|
||||
suffix=".yaml", delete=False)
|
||||
file_1.write(b"key1: one")
|
||||
file_1.close()
|
||||
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
|
||||
suffix=".yaml", delete=False)
|
||||
file_2.write(b"key2: two\nkey3: three")
|
||||
file_2.close()
|
||||
conf = "key: !include_dir_merge_named {}".format(include_dir)
|
||||
with io.StringIO(conf) as f:
|
||||
doc = yaml.yaml.safe_load(f)
|
||||
assert doc["key"] == {
|
||||
"key1": "one",
|
||||
"key2": "two",
|
||||
"key3": "three"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user