Merge pull request #2113 from home-assistant/dev

0.20
This commit is contained in:
Paulus Schoutsen 2016-05-21 14:15:42 -07:00
commit b78765a41f
90 changed files with 3147 additions and 389 deletions

View File

@ -38,6 +38,9 @@ omit =
homeassistant/components/octoprint.py homeassistant/components/octoprint.py
homeassistant/components/*/octoprint.py homeassistant/components/*/octoprint.py
homeassistant/components/qwikswitch.py
homeassistant/components/*/qwikswitch.py
homeassistant/components/rpi_gpio.py homeassistant/components/rpi_gpio.py
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/cast.py
homeassistant/components/media_player/denon.py homeassistant/components/media_player/denon.py
homeassistant/components/media_player/firetv.py homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/itunes.py homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/mpd.py homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/samsungtv.py homeassistant/components/media_player/samsungtv.py
homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/yamaha.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/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/googlevoice.py homeassistant/components/notify/googlevoice.py
@ -138,6 +147,7 @@ omit =
homeassistant/components/notify/smtp.py homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py homeassistant/components/notify/telegram.py
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twitter.py homeassistant/components/notify/twitter.py
homeassistant/components/notify/xmpp.py homeassistant/components/notify/xmpp.py
homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/hunterdouglas_powerview.py
@ -153,6 +163,7 @@ omit =
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/netatmo.py homeassistant/components/sensor/netatmo.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
@ -163,6 +174,7 @@ omit =
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py homeassistant/components/sensor/temper.py

View File

@ -3,6 +3,8 @@
**Related issue (if applicable):** # **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):** **Example entry for `configuration.yaml` (if applicable):**
```yaml ```yaml
@ -10,6 +12,9 @@
**Checklist:** **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: If code communicates with devices:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass** - [ ] 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]). - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).

2
.gitignore vendored
View File

@ -83,3 +83,5 @@ venv
# vimmy stuff # vimmy stuff
*.swp *.swp
*.swo *.swo
ctags.tmp

View File

@ -21,6 +21,14 @@ RUN script/build_python_openzwave && \
COPY requirements_all.txt requirements_all.txt COPY requirements_all.txt requirements_all.txt
RUN pip3 install --no-cache-dir -r 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 source
COPY . . COPY . .

View File

@ -3,11 +3,12 @@ from __future__ import print_function
import argparse import argparse
import os import os
import platform
import signal import signal
import subprocess
import sys import sys
import threading import threading
import time import time
from multiprocessing import Process
from homeassistant.const import ( from homeassistant.const import (
__version__, __version__,
@ -87,8 +88,7 @@ def get_arguments():
parser.add_argument( parser.add_argument(
'--debug', '--debug',
action='store_true', action='store_true',
help='Start Home Assistant in debug mode. Runs in single process to ' help='Start Home Assistant in debug mode')
'enable use of interactive debuggers.')
parser.add_argument( parser.add_argument(
'--open-ui', '--open-ui',
action='store_true', action='store_true',
@ -123,15 +123,20 @@ def get_arguments():
'--restart-osx', '--restart-osx',
action='store_true', action='store_true',
help='Restarts on OS X.') 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( parser.add_argument(
'--daemon', '--daemon',
action='store_true', action='store_true',
help='Run Home Assistant as daemon') help='Run Home Assistant as daemon')
arguments = parser.parse_args() arguments = parser.parse_args()
if os.name == "nt": if os.name != "posix" or arguments.debug or arguments.runner:
arguments.daemon = False arguments.daemon = False
return arguments return arguments
@ -144,13 +149,21 @@ def daemonize():
# Decouple fork # Decouple fork
os.setsid() os.setsid()
os.umask(0)
# Create second fork # Create second fork
pid = os.fork() pid = os.fork()
if pid > 0: if pid > 0:
sys.exit(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): def check_pid(pid_file):
"""Check that HA is not already running.""" """Check that HA is not already running."""
@ -161,6 +174,10 @@ def check_pid(pid_file):
# PID File does not exist # PID File does not exist
return return
# If we just restarted, we just found our own pidfile.
if pid == os.getpid():
return
try: try:
os.kill(pid, 0) os.kill(pid, 0)
except OSError: except OSError:
@ -220,29 +237,61 @@ def uninstall_osx():
print("Home Assistant has been uninstalled.") print("Home Assistant has been uninstalled.")
def setup_and_run_hass(config_dir, args, top_process=False): def closefds_osx(min_fd, max_fd):
"""Setup HASS and run. """Make sure file descriptors get closed when we restart.
Block until stopped. Will assume it is running in a subprocess unless We cannot call close on guarded fds, and we cannot easily test which fds
top_process is set to true. 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 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: if args.demo_mode:
config = { config = {
'frontend': {}, 'frontend': {},
'demo': {} 'demo': {}
} }
hass = bootstrap.from_config_dict( hass = bootstrap.from_config_dict(
config, config_dir=config_dir, daemon=args.daemon, config, config_dir=config_dir, verbose=args.verbose,
verbose=args.verbose, skip_pip=args.skip_pip, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
log_rotate_days=args.log_rotate_days)
else: else:
config_file = ensure_config_file(config_dir) config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir) print('Config directory:', config_dir)
hass = bootstrap.from_config_file( hass = bootstrap.from_config_file(
config_file, daemon=args.daemon, verbose=args.verbose, config_file, verbose=args.verbose, skip_pip=args.skip_pip,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) log_rotate_days=args.log_rotate_days)
if hass is None: if hass is None:
return 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) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
print('Starting Home-Assistant')
hass.start() hass.start()
exit_code = int(hass.block_till_stopped()) exit_code = int(hass.block_till_stopped())
if not top_process:
sys.exit(exit_code)
return exit_code return exit_code
def run_hass_process(hass_proc): def try_to_restart():
"""Run a child hass process. Returns True if it should be restarted.""" """Attempt to clean up state and start a new homeassistant instance."""
requested_stop = threading.Event() # Things should be mostly shut down already at this point, now just try
hass_proc.daemon = True # to clean up things that may have been left behind.
sys.stderr.write('Home Assistant attempting to restart.\n')
def request_stop(*args): # Count remaining threads, ideally there should only be one non-daemonized
"""Request hass stop, *args is for signal handler callback.""" # thread left (which is us). Nothing we really do with it, but it might be
requested_stop.set() # useful when debugging shutdown/restart issues.
hass_proc.terminate() 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: # Send terminate signal to all processes in our process group which
signal.signal(signal.SIGTERM, request_stop) # should be any children that have not themselves changed the process
except ValueError: # group id. Don't bother if couldn't even call setpgid.
print('Could not bind to SIGTERM. Are you running in a thread?') if hasattr(os, 'setpgid'):
sys.stderr.write("Signalling child processes to terminate...\n")
os.kill(0, signal.SIGTERM)
hass_proc.start() # wait for child processes to terminate
try:
hass_proc.join()
except KeyboardInterrupt:
request_stop()
try: try:
hass_proc.join() while True:
except KeyboardInterrupt: time.sleep(1)
return False if os.waitpid(0, os.WNOHANG) == (0, 0):
break
except OSError:
pass
return (not requested_stop.isSet() and elif os.name == 'nt':
hass_proc.exitcode == RESTART_EXIT_CODE, # Maybe one of the following will work, but how do we indicate which
hass_proc.exitcode) # 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(): def main():
@ -325,21 +400,17 @@ def main():
if args.pid_file: if args.pid_file:
write_pid(args.pid_file) write_pid(args.pid_file)
# Run hass in debug mode if requested # Create new process group if we can
if args.debug: if hasattr(os, 'setpgid'):
sys.stderr.write('Running in debug mode. ' try:
'Home Assistant will not be able to restart.\n') os.setpgid(0, 0)
exit_code = setup_and_run_hass(config_dir, args, top_process=True) except PermissionError:
if exit_code == RESTART_EXIT_CODE: pass
sys.stderr.write('Home Assistant requested a '
'restart in debug mode.\n') exit_code = setup_and_run_hass(config_dir, args)
return exit_code 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 return exit_code

View File

@ -215,7 +215,7 @@ def mount_local_lib_path(config_dir):
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments # pylint: disable=too-many-branches, too-many-statements, too-many-arguments
def from_config_dict(config, hass=None, config_dir=None, enable_log=True, 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): log_rotate_days=None):
"""Try to configure Home Assistant from a config dict. """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) process_ha_config_upgrade(hass)
if enable_log: if enable_log:
enable_logging(hass, verbose, daemon, log_rotate_days) enable_logging(hass, verbose, log_rotate_days)
hass.config.skip_pip = skip_pip hass.config.skip_pip = skip_pip
if skip_pip: if skip_pip:
@ -278,8 +278,8 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
return hass return hass
def from_config_file(config_path, hass=None, verbose=False, daemon=False, def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
skip_pip=True, log_rotate_days=None): log_rotate_days=None):
"""Read the configuration file and try to start all the functionality. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given, 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 hass.config.config_dir = config_dir
mount_local_lib_path(config_dir) mount_local_lib_path(config_dir)
enable_logging(hass, verbose, daemon, log_rotate_days) enable_logging(hass, verbose, log_rotate_days)
try: try:
config_dict = config_util.load_yaml_config_file(config_path) 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) 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.""" """Setup the logging."""
if not daemon: logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.INFO) fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s%(reset)s")
"[%(name)s] %(message)s%(reset)s") try:
try: from colorlog import ColoredFormatter
from colorlog import ColoredFormatter logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
logging.getLogger().handlers[0].setFormatter(ColoredFormatter( fmt,
fmt, datefmt='%y-%m-%d %H:%M:%S',
datefmt='%y-%m-%d %H:%M:%S', reset=True,
reset=True, log_colors={
log_colors={ 'DEBUG': 'cyan',
'DEBUG': 'cyan', 'INFO': 'green',
'INFO': 'green', 'WARNING': 'yellow',
'WARNING': 'yellow', 'ERROR': 'red',
'ERROR': 'red', 'CRITICAL': 'red',
'CRITICAL': 'red', }
} ))
)) except ImportError:
except ImportError: pass
pass
# Log errors to a file if we have write access to file or config dir # Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path(ERROR_LOG_FILENAME) err_log_path = hass.config.path(ERROR_LOG_FILENAME)

View File

@ -16,7 +16,7 @@ from homeassistant.helpers import condition, config_validation as cv
TRIGGER_SCHEMA = vol.All(vol.Schema({ TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'numeric_state', 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_BELOW: vol.Coerce(float),
CONF_ABOVE: vol.Coerce(float), CONF_ABOVE: vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
@ -41,7 +41,7 @@ def trigger(hass, config, action):
variables = { variables = {
'trigger': { 'trigger': {
'platform': 'numeric_state', 'platform': 'numeric_state',
'entity_id': entity_id, 'entity_id': entity,
'below': below, 'below': below,
'above': above, 'above': above,
} }

View File

@ -19,6 +19,8 @@ SERVICE_CONFIGURE = "configure"
STATE_CONFIGURE = "configure" STATE_CONFIGURE = "configure"
STATE_CONFIGURED = "configured" STATE_CONFIGURED = "configured"
ATTR_LINK_NAME = "link_name"
ATTR_LINK_URL = "link_url"
ATTR_CONFIGURE_ID = "configure_id" ATTR_CONFIGURE_ID = "configure_id"
ATTR_DESCRIPTION = "description" ATTR_DESCRIPTION = "description"
ATTR_DESCRIPTION_IMAGE = "description_image" ATTR_DESCRIPTION_IMAGE = "description_image"
@ -34,7 +36,7 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def request_config( def request_config(
hass, name, callback, description=None, description_image=None, 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. """Create a new request for configuration.
Will return an ID to be used for sequent calls. Will return an ID to be used for sequent calls.
@ -43,7 +45,8 @@ def request_config(
request_id = instance.request_config( request_id = instance.request_config(
name, callback, name, callback,
description, description_image, submit_caption, fields) description, description_image, submit_caption,
fields, link_name, link_url)
_REQUESTS[request_id] = instance _REQUESTS[request_id] = instance
@ -100,7 +103,8 @@ class Configurator(object):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def request_config( def request_config(
self, name, callback, self, name, callback,
description, description_image, submit_caption, fields): description, description_image, submit_caption,
fields, link_name, link_url):
"""Setup a request for configuration.""" """Setup a request for configuration."""
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass) entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
@ -121,6 +125,8 @@ class Configurator(object):
(ATTR_DESCRIPTION, description), (ATTR_DESCRIPTION, description),
(ATTR_DESCRIPTION_IMAGE, description_image), (ATTR_DESCRIPTION_IMAGE, description_image),
(ATTR_SUBMIT_CAPTION, submit_caption), (ATTR_SUBMIT_CAPTION, submit_caption),
(ATTR_LINK_NAME, link_name),
(ATTR_LINK_URL, link_url),
] if value is not None ] if value is not None
}) })

View File

@ -21,6 +21,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
'camera', 'camera',
'device_tracker', 'device_tracker',
'garage_door', 'garage_door',
'hvac',
'light', 'light',
'lock', 'lock',
'media_player', 'media_player',

View File

@ -19,13 +19,16 @@ from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pexpect==4.0.1']
_LEASES_CMD = 'cat /var/lib/misc/dnsmasq.leases'
_LEASES_REGEX = re.compile( _LEASES_REGEX = re.compile(
r'\w+\s' + r'\w+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\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<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'(?P<host>([^\s]+))') r'(?P<host>([^\s]+))')
_IP_NEIGH_CMD = 'ip neigh'
_IP_NEIGH_REGEX = re.compile( _IP_NEIGH_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' + r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'\w+\s' + r'\w+\s' +
@ -55,6 +58,7 @@ class AsusWrtDeviceScanner(object):
self.host = config[CONF_HOST] self.host = config[CONF_HOST]
self.username = str(config[CONF_USERNAME]) self.username = str(config[CONF_USERNAME])
self.password = str(config[CONF_PASSWORD]) self.password = str(config[CONF_PASSWORD])
self.protocol = config.get('protocol')
self.lock = threading.Lock() self.lock = threading.Lock()
@ -100,8 +104,26 @@ class AsusWrtDeviceScanner(object):
self.last_results = active_clients self.last_results = active_clients
return True return True
def get_asuswrt_data(self): def ssh_connection(self):
"""Retrieve data from ASUSWRT and return parsed result.""" """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: try:
telnet = telnetlib.Telnet(self.host) telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'login: ') telnet.read_until(b'login: ')
@ -109,18 +131,26 @@ class AsusWrtDeviceScanner(object):
telnet.read_until(b'Password: ') telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii')) telnet.write((self.password + '\n').encode('ascii'))
prompt_string = telnet.read_until(b'#').split(b'\n')[-1] 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] 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] leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii')) telnet.write('exit\n'.encode('ascii'))
return (neighbors, leases_result)
except EOFError: except EOFError:
_LOGGER.exception("Unexpected response from router") _LOGGER.exception("Unexpected response from router")
return return ('', '')
except ConnectionRefusedError: except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," + _LOGGER.exception("Connection refused by router,"
" is telnet enabled?") " 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 = {} devices = {}
for lease in leases_result: for lease in leases_result:

View File

@ -11,7 +11,7 @@ from collections import defaultdict
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME from homeassistant.const import STATE_HOME
from homeassistant.util import convert from homeassistant.util import convert, slugify
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
@ -53,6 +53,12 @@ def setup_scanner(hass, config, see):
'accuracy %s is not met: %s', 'accuracy %s is not met: %s',
data_type, max_gps_accuracy, data) data_type, max_gps_accuracy, data)
return None 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 return data
def owntracks_location_update(topic, payload, qos): def owntracks_location_update(topic, payload, qos):
@ -91,7 +97,7 @@ def setup_scanner(hass, config, see):
return return
# OwnTracks uses - at the start of a beacon zone # OwnTracks uses - at the start of a beacon zone
# to switch on 'hold mode' - ignore this # to switch on 'hold mode' - ignore this
location = data['desc'].lstrip("-") location = slugify(data['desc'].lstrip("-"))
if location.lower() == 'home': if location.lower() == 'home':
location = STATE_HOME location = STATE_HOME

View File

@ -15,10 +15,12 @@ from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED) EVENT_PLATFORM_DISCOVERED)
DOMAIN = "discovery" DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.6.6'] REQUIREMENTS = ['netdisco==0.6.7']
SCAN_INTERVAL = 300 # seconds SCAN_INTERVAL = 300 # seconds
LOAD_PLATFORM = 'load_platform'
SERVICE_WEMO = 'belkin_wemo' SERVICE_WEMO = 'belkin_wemo'
SERVICE_HUE = 'philips_hue' SERVICE_HUE = 'philips_hue'
SERVICE_CAST = 'google_cast' SERVICE_CAST = 'google_cast'
@ -27,6 +29,7 @@ SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver' SERVICE_PLEX = 'plex_mediaserver'
SERVICE_SQUEEZEBOX = 'logitech_mediaserver' SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
SERVICE_PANASONIC_VIERA = 'panasonic_viera' SERVICE_PANASONIC_VIERA = 'panasonic_viera'
SERVICE_ROKU = 'roku'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_WEMO: "wemo", SERVICE_WEMO: "wemo",
@ -37,6 +40,7 @@ SERVICE_HANDLERS = {
SERVICE_PLEX: 'media_player', SERVICE_PLEX: 'media_player',
SERVICE_SQUEEZEBOX: 'media_player', SERVICE_SQUEEZEBOX: 'media_player',
SERVICE_PANASONIC_VIERA: '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): def discovery_event_listener(event):
"""Listen for discovery events.""" """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)) callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener) 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) 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): def setup(hass, config):
"""Start a discovery service.""" """Start a discovery service."""
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -22,7 +22,7 @@ HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [ REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/' '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__) _LOGGER = logging.getLogger(__name__)

View File

@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script.""" """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

View File

@ -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})})}))})}});

View File

@ -29,7 +29,7 @@ SERVICE_SET_AUX_HEAT = "set_aux_heat"
SERVICE_SET_TEMPERATURE = "set_temperature" SERVICE_SET_TEMPERATURE = "set_temperature"
SERVICE_SET_FAN_MODE = "set_fan_mode" SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_OPERATION_MODE = "set_operation_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" SERVICE_SET_HUMIDITY = "set_humidity"
STATE_HEAT = "heat" STATE_HEAT = "heat"
@ -40,17 +40,17 @@ STATE_DRY = "dry"
STATE_FAN_ONLY = "fan_only" STATE_FAN_ONLY = "fan_only"
ATTR_CURRENT_TEMPERATURE = "current_temperature" 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_MAX_TEMP = "max_temp"
ATTR_MIN_TEMP = "min_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_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity" ATTR_MIN_HUMIDITY = "min_humidity"
ATTR_OPERATION = "operation_mode" ATTR_OPERATION_MODE = "operation_mode"
ATTR_OPERATION_LIST = "operation_list" ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode" ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_LIST = "swing_list" 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): def set_fan_mode(hass, fan, entity_id=None):
"""Turn all or specified hvac fan mode on.""" """Turn all or specified hvac fan mode on."""
data = {ATTR_FAN: fan} data = {ATTR_FAN_MODE: fan}
if entity_id: if entity_id:
data[ATTR_ENTITY_ID] = 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): def set_operation_mode(hass, operation_mode, entity_id=None):
"""Set new target operation mode.""" """Set new target operation mode."""
data = {ATTR_OPERATION: operation_mode} data = {ATTR_OPERATION_MODE: operation_mode}
if entity_id is not None: if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id 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: if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id 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 # pylint: disable=too-many-branches
@ -247,12 +247,12 @@ def setup(hass, config):
"""Set fan mode on target hvacs.""" """Set fan mode on target hvacs."""
target_hvacs = component.extract_from_service(service) target_hvacs = component.extract_from_service(service)
fan = service.data.get(ATTR_FAN) fan = service.data.get(ATTR_FAN_MODE)
if fan is None: if fan is None:
_LOGGER.error( _LOGGER.error(
"Received call to %s without attribute %s", "Received call to %s without attribute %s",
SERVICE_SET_FAN_MODE, ATTR_FAN) SERVICE_SET_FAN_MODE, ATTR_FAN_MODE)
return return
for hvac in target_hvacs: for hvac in target_hvacs:
@ -269,16 +269,16 @@ def setup(hass, config):
"""Set operating mode on the target hvacs.""" """Set operating mode on the target hvacs."""
target_hvacs = component.extract_from_service(service) 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: if operation_mode is None:
_LOGGER.error( _LOGGER.error(
"Received call to %s without attribute %s", "Received call to %s without attribute %s",
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION) SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE)
return return
for hvac in target_hvacs: for hvac in target_hvacs:
hvac.set_operation(operation_mode) hvac.set_operation_mode(operation_mode)
if hvac.should_poll: if hvac.should_poll:
hvac.update_ha_state(True) hvac.update_ha_state(True)
@ -296,18 +296,18 @@ def setup(hass, config):
if swing_mode is None: if swing_mode is None:
_LOGGER.error( _LOGGER.error(
"Received call to %s without attribute %s", "Received call to %s without attribute %s",
SERVICE_SET_SWING, ATTR_SWING_MODE) SERVICE_SET_SWING_MODE, ATTR_SWING_MODE)
return return
for hvac in target_hvacs: for hvac in target_hvacs:
hvac.set_swing(swing_mode) hvac.set_swing_mode(swing_mode)
if hvac.should_poll: if hvac.should_poll:
hvac.update_ha_state(True) hvac.update_ha_state(True)
hass.services.register( hass.services.register(
DOMAIN, SERVICE_SET_SWING, swing_set_service, DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
descriptions.get(SERVICE_SET_SWING)) descriptions.get(SERVICE_SET_SWING_MODE))
return True return True
@ -330,19 +330,30 @@ class HvacDevice(Entity):
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp), ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
ATTR_TEMPERATURE: ATTR_TEMPERATURE:
self._convert_for_display(self.target_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 is_away = self.is_away_mode_on
if is_away is not None: if is_away is not None:
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
@ -430,11 +441,11 @@ class HvacDevice(Entity):
"""Set new target fan mode.""" """Set new target fan mode."""
pass pass
def set_operation(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target operation mode.""" """Set new target operation mode."""
pass pass
def set_swing(self, swing_mode): def set_swing_mode(self, swing_mode):
"""Set new target swing operation.""" """Set new target swing operation."""
pass pass
@ -457,12 +468,12 @@ class HvacDevice(Entity):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self._convert_for_display(7) return convert(7, TEMP_CELCIUS, self.unit_of_measurement)
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self._convert_for_display(35) return convert(35, TEMP_CELCIUS, self.unit_of_measurement)
@property @property
def min_humidity(self): def min_humidity(self):

View File

@ -118,7 +118,7 @@ class DemoHvac(HvacDevice):
self._target_humidity = humidity self._target_humidity = humidity
self.update_ha_state() self.update_ha_state()
def set_swing(self, swing_mode): def set_swing_mode(self, swing_mode):
"""Set new target temperature.""" """Set new target temperature."""
self._current_swing_mode = swing_mode self._current_swing_mode = swing_mode
self.update_ha_state() self.update_ha_state()
@ -128,7 +128,7 @@ class DemoHvac(HvacDevice):
self._current_fan_mode = fan self._current_fan_mode = fan
self.update_ha_state() self.update_ha_state()
def set_operation(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target temperature.""" """Set new target temperature."""
self._current_operation = operation_mode self._current_operation = operation_mode
self.update_ha_state() self.update_ha_state()

45
homeassistant/components/hvac/zwave.py Normal file → Executable file
View 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 # Because we do not compile openzwave on CI
# pylint: disable=import-error # pylint: disable=import-error
import logging import logging
@ -19,6 +23,12 @@ REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377 REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0) 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' WORKAROUND_ZXT_120 = 'zxt_120'
DEVICE_MAPPINGS = { DEVICE_MAPPINGS = {
@ -96,22 +106,24 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def update_properties(self): def update_properties(self):
"""Callback on data change for the registered node/value pair.""" """Callback on data change for the registered node/value pair."""
# Set point # 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: if int(value.data) != 0:
self._target_temperature = int(value.data) self._target_temperature = int(value.data)
# Operation Mode # 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._current_operation = value.data
self._operation_list = list(value.data_items) self._operation_list = list(value.data_items)
_LOGGER.debug("self._operation_list=%s", self._operation_list) _LOGGER.debug("self._operation_list=%s", self._operation_list)
# Current Temp # 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._current_temperature = int(value.data)
self._unit = value.units self._unit = value.units
# Fan Mode # Fan Mode
fan_class_id = 0x44 if self._zxt_120 else 0x42 for value in self._node.get_values(
_LOGGER.debug("fan_class_id=%s", fan_class_id) class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
for value in self._node.get_values(class_id=fan_class_id).values():
self._current_operation_state = value.data self._current_operation_state = value.data
self._fan_list = list(value.data_items) self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list) _LOGGER.debug("self._fan_list=%s", self._fan_list)
@ -119,7 +131,8 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
self._current_operation_state) self._current_operation_state)
# Swing mode # Swing mode
if self._zxt_120 == 1: 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: if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data self._current_swing_mode = value.data
self._swing_list = [0, 1] self._swing_list = [0, 1]
@ -184,7 +197,8 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def set_temperature(self, temperature): def set_temperature(self, temperature):
"""Set new target 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: if value.command_class != 67:
continue continue
if self._zxt_120: if self._zxt_120:
@ -200,20 +214,23 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def set_fan_mode(self, fan): def set_fan_mode(self, fan):
"""Set new target fan mode.""" """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: if value.command_class == 68 and value.index == 0:
value.data = bytes(fan, 'utf-8') value.data = bytes(fan, 'utf-8')
def set_operation(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target 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: if value.command_class == 64 and value.index == 0:
value.data = bytes(operation_mode, 'utf-8') 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.""" """Set new target swing mode."""
if self._zxt_120 == 1: 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: if value.command_class == 112 and value.index == 33:
value.data = int(swing_mode) value.data = int(swing_mode)

View File

@ -39,6 +39,7 @@ ATTR_TRANSITION = "transition"
ATTR_RGB_COLOR = "rgb_color" ATTR_RGB_COLOR = "rgb_color"
ATTR_XY_COLOR = "xy_color" ATTR_XY_COLOR = "xy_color"
ATTR_COLOR_TEMP = "color_temp" ATTR_COLOR_TEMP = "color_temp"
ATTR_COLOR_NAME = "color_name"
# int with value 0 .. 255 representing brightness of the light. # int with value 0 .. 255 representing brightness of the light.
ATTR_BRIGHTNESS = "brightness" ATTR_BRIGHTNESS = "brightness"
@ -87,6 +88,7 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_PROFILE: str, ATTR_PROFILE: str,
ATTR_TRANSITION: VALID_TRANSITION, ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: cv.byte, ATTR_BRIGHTNESS: cv.byte,
ATTR_COLOR_NAME: str,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)), vol.Coerce(tuple)),
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), 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 # pylint: disable=too-many-arguments
def turn_on(hass, entity_id=None, transition=None, brightness=None, def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None, profile=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.""" """Turn all or specified light on."""
data = { data = {
key: value for key, value in [ 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_COLOR_TEMP, color_temp),
(ATTR_FLASH, flash), (ATTR_FLASH, flash),
(ATTR_EFFECT, effect), (ATTR_EFFECT, effect),
(ATTR_COLOR_NAME, color_name),
] if value is not None ] if value is not None
} }
@ -228,6 +231,11 @@ def setup(hass, config):
params.setdefault(ATTR_XY_COLOR, profile[:2]) params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, 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: for light in target_lights:
light.turn_on(**params) light.turn_on(**params)

View File

@ -235,14 +235,16 @@ class HueLight(Light):
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10 command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10
if ATTR_BRIGHTNESS in kwargs:
command['bri'] = kwargs[ATTR_BRIGHTNESS]
if ATTR_XY_COLOR in kwargs: if ATTR_XY_COLOR in kwargs:
command['xy'] = kwargs[ATTR_XY_COLOR] command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs: 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])) *(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: if ATTR_COLOR_TEMP in kwargs:
command['ct'] = kwargs[ATTR_COLOR_TEMP] command['ct'] = kwargs[ATTR_COLOR_TEMP]

View 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

View File

@ -16,6 +16,10 @@ turn_on:
description: Color for the light in RGB-format description: Color for the light in RGB-format
example: '[255, 100, 100]' example: '[255, 100, 100]'
color_name:
description: A human readable color name
example: 'red'
xy_color: xy_color:
description: Color for the light in XY-format description: Color for the light in XY-format
example: '[0.52, 0.43]' example: '[0.52, 0.43]'

View File

@ -105,6 +105,7 @@ class WemoLight(Light):
elif ATTR_RGB_COLOR in kwargs: elif ATTR_RGB_COLOR in kwargs:
xycolor = color_util.color_RGB_to_xy( xycolor = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR])) *(int(val) for val in kwargs[ATTR_RGB_COLOR]))
kwargs.setdefault(ATTR_BRIGHTNESS, xycolor[2])
else: else:
xycolor = None xycolor = None

View File

@ -97,7 +97,9 @@ class WinkLight(Light):
} }
if rgb_color: 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: if color_temp_mired:
state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired) state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired)

View 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

View File

@ -36,6 +36,7 @@ DISCOVERY_PLATFORMS = {
discovery.SERVICE_PLEX: 'plex', discovery.SERVICE_PLEX: 'plex',
discovery.SERVICE_SQUEEZEBOX: 'squeezebox', discovery.SERVICE_SQUEEZEBOX: 'squeezebox',
discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera', discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera',
discovery.SERVICE_ROKU: 'roku',
} }
SERVICE_PLAY_MEDIA = 'play_media' SERVICE_PLAY_MEDIA = 'play_media'
@ -62,6 +63,7 @@ ATTR_APP_NAME = 'app_name'
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands' ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
ATTR_INPUT_SOURCE = 'source' ATTR_INPUT_SOURCE = 'source'
ATTR_INPUT_SOURCE_LIST = 'source_list' ATTR_INPUT_SOURCE_LIST = 'source_list'
ATTR_MEDIA_ENQUEUE = 'enqueue'
MEDIA_TYPE_MUSIC = 'music' MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow' 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({ MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
ATTR_MEDIA_ENQUEUE: cv.boolean,
}) })
MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ 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) 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.""" """Send the media player the command for playing media."""
data = {ATTR_MEDIA_CONTENT_TYPE: media_type, data = {ATTR_MEDIA_CONTENT_TYPE: media_type,
ATTR_MEDIA_CONTENT_ID: media_id} ATTR_MEDIA_CONTENT_ID: media_id}
@ -263,6 +266,9 @@ def play_media(hass, media_type, media_id, entity_id=None):
if entity_id: if entity_id:
data[ATTR_ENTITY_ID] = entity_id data[ATTR_ENTITY_ID] = entity_id
if enqueue:
data[ATTR_MEDIA_ENQUEUE] = enqueue
hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data) hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data)
@ -363,9 +369,14 @@ def setup(hass, config):
"""Play specified media_id on the media player.""" """Play specified media_id on the media player."""
media_type = service.data.get(ATTR_MEDIA_CONTENT_TYPE) media_type = service.data.get(ATTR_MEDIA_CONTENT_TYPE)
media_id = service.data.get(ATTR_MEDIA_CONTENT_ID) 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): 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: if player.should_poll:
player.update_ha_state(True) player.update_ha_state(True)

View File

@ -253,7 +253,7 @@ class CastDevice(MediaPlayerDevice):
"""Seek the media to a specific location.""" """Seek the media to a specific location."""
self.cast.media_controller.seek(position) 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.""" """Play media from a URL."""
self.cast.media_controller.play_media(media_id, media_type) self.cast.media_controller.play_media(media_id, media_type)

View File

@ -152,7 +152,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
"""Flag of media commands that are supported.""" """Flag of media commands that are supported."""
return YOUTUBE_PLAYER_SUPPORT 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.""" """Play a piece of media."""
self.youtube_id = media_id self.youtube_id = media_id
self.update_ha_state() self.update_ha_state()

View 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()

View File

@ -320,7 +320,7 @@ class ItunesDevice(MediaPlayerDevice):
response = self.client.previous() response = self.client.previous()
self.update_state(response) 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.""" """Send the play_media command to the media player."""
if media_type == MEDIA_TYPE_PLAYLIST: if media_type == MEDIA_TYPE_PLAYLIST:
response = self.client.play_playlist(media_id) response = self.client.play_playlist(media_id)

View File

@ -278,6 +278,6 @@ class KodiDevice(MediaPlayerDevice):
self.update_ha_state() 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.""" """Send the play_media command to the media player."""
self._server.Player.Open({media_type: media_id}, {}) self._server.Player.Open({media_type: media_id}, {})

View 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)

View File

@ -89,7 +89,13 @@ class MpdDevice(MediaPlayerDevice):
try: try:
self.status = self.client.status() self.status = self.client.status()
self.currentsong = self.client.currentsong() 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) self.client.connect(self.server, self.port)
if self.password is not None: if self.password is not None:
@ -206,7 +212,7 @@ class MpdDevice(MediaPlayerDevice):
"""Service to send the MPD the command for previous track.""" """Service to send the MPD the command for previous track."""
self.client.previous() 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.""" """Send the media player the command for playing a playlist."""
_LOGGER.info(str.format("Playing playlist: {0}", media_id)) _LOGGER.info(str.format("Playing playlist: {0}", media_id))
if media_type == MEDIA_TYPE_PLAYLIST: if media_type == MEDIA_TYPE_PLAYLIST:

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice) 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/' REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/'
'python3.zip#onkyo-eiscp==0.9.2'] 'python3.zip#onkyo-eiscp==0.9.2']
@ -17,29 +17,59 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE 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): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Onkyo platform.""" """Setup the Onkyo platform."""
import eiscp
from eiscp import eISCP from eiscp import eISCP
add_devices(OnkyoDevice(receiver) hosts = []
for receiver in eISCP.discover())
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): class OnkyoDevice(MediaPlayerDevice):
"""Representation of a Onkyo device.""" """Representation of a Onkyo device."""
# pylint: disable=too-many-public-methods, abstract-method # pylint: disable=too-many-public-methods, abstract-method
def __init__(self, receiver): def __init__(self, receiver, sources, name=None):
"""Initialize the Onkyo Receiver.""" """Initialize the Onkyo Receiver."""
self._receiver = receiver self._receiver = receiver
self._muted = False self._muted = False
self._volume = 0 self._volume = 0
self._pwstate = STATE_OFF self._pwstate = STATE_OFF
self.update() self._name = name or '{}_{}'.format(
self._name = '{}_{}'.format(
receiver.info['model_name'], receiver.info['identifier']) receiver.info['model_name'], receiver.info['identifier'])
self._current_source = None 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): def update(self):
"""Get the latest details from the device.""" """Get the latest details from the device."""
@ -52,8 +82,13 @@ class OnkyoDevice(MediaPlayerDevice):
volume_raw = self._receiver.command('volume query') volume_raw = self._receiver.command('volume query')
mute_raw = self._receiver.command('audio-muting query') mute_raw = self._receiver.command('audio-muting query')
current_source_raw = self._receiver.command('input-selector query') current_source_raw = self._receiver.command('input-selector query')
self._current_source = '_'.join('_'.join( for source in current_source_raw[1]:
[i for i 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._muted = bool(mute_raw[1] == 'on')
self._volume = int(volume_raw[1], 16)/80.0 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 the current input source of the device."""
return self._current_source return self._current_source
@property
def source_list(self):
"""List of available input sources."""
return self._source_list
def turn_off(self): def turn_off(self):
"""Turn off media player.""" """Turn off media player."""
self._receiver.command('system-power standby') self._receiver.command('system-power standby')
@ -108,4 +148,6 @@ class OnkyoDevice(MediaPlayerDevice):
def select_source(self, source): def select_source(self, source):
"""Set the input source.""" """Set the input source."""
if source in self._source_list:
source = self._reverse_mapping[source]
self._receiver.command('input-selector {}'.format(source)) self._receiver.command('input-selector {}'.format(source))

View 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()

View File

@ -145,3 +145,11 @@ select_source:
source: source:
description: Name of the source to switch to. Platform dependent. description: Name of the source to switch to. Platform dependent.
example: 'video1' 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'

View File

@ -7,14 +7,15 @@ https://home-assistant.io/components/media_player.sonos/
import datetime import datetime
import logging import logging
import socket import socket
from os import path
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF) STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['SoCo==0.11.1'] 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_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
SUPPORT_SEEK SUPPORT_SEEK
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): 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.') _LOGGER.warning('No Sonos speakers found.')
return False 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)) _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 return True
@ -74,16 +99,26 @@ def only_if_coordinator(func):
If used as decorator, avoid calling the decorated method if player is not 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 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): def wrapper(*args, **kwargs):
"""Decorator wrapper.""" """Decorator wrapper."""
if args[0].is_coordinator: 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: else:
_LOGGER.debug('Ignore command "%s" for Sonos device "%s" ' _LOGGER.debug('Ignore command "%s" for Sonos device "%s" (%s)',
'(not coordinator)', func.__name__, args[0].name, 'not coordinator')
func.__name__, args[0].name)
return wrapper return wrapper
@ -104,7 +139,7 @@ class SonosDevice(MediaPlayerDevice):
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed.""" """Polling needed."""
return True return True
def update_sonos(self, now): def update_sonos(self, now):
@ -258,9 +293,27 @@ class SonosDevice(MediaPlayerDevice):
self._player.play() self._player.play()
@only_if_coordinator @only_if_coordinator
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._player.play_uri(media_id) 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 @property
def available(self): def available(self):

View File

@ -402,7 +402,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
data = {ATTR_MEDIA_SEEK_POSITION: position} data = {ATTR_MEDIA_SEEK_POSITION: position}
self._call_service(SERVICE_MEDIA_SEEK, data) 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.""" """Play a piece of media."""
data = {ATTR_MEDIA_CONTENT_TYPE: media_type, data = {ATTR_MEDIA_CONTENT_TYPE: media_type,
ATTR_MEDIA_CONTENT_ID: media_id} ATTR_MEDIA_CONTENT_ID: media_id}

View File

@ -388,6 +388,8 @@ class MQTT(object):
def _mqtt_on_message(self, _mqttc, _userdata, msg): def _mqtt_on_message(self, _mqttc, _userdata, msg):
"""Message received callback.""" """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, { self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
ATTR_TOPIC: msg.topic, ATTR_TOPIC: msg.topic,
ATTR_QOS: msg.qos, ATTR_QOS: msg.qos,

View File

@ -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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.nest/ https://home-assistant.io/components/thermostat.nest/
@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
REQUIREMENTS = ['python-nest==2.6.0'] REQUIREMENTS = ['python-nest==2.9.2']
DOMAIN = 'nest' DOMAIN = 'nest'
NEST = None NEST = None
@ -36,6 +36,16 @@ def devices():
_LOGGER.error("Connection error logging into the nest web service.") _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 # pylint: disable=unused-argument
def setup(hass, config): def setup(hass, config):
"""Setup the Nest thermostat component.""" """Setup the Nest thermostat component."""

View 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)

View 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)

View 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)

View 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)

View File

@ -41,8 +41,9 @@ class GNTPNotificationService(BaseNotificationService):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, app_name, app_icon, hostname, password, port): def __init__(self, app_name, app_icon, hostname, password, port):
"""Initialize the service.""" """Initialize the service."""
from gntp import notifier import gntp.notifier
self.gntp = notifier.GrowlNotifier( import gntp.errors
self.gntp = gntp.notifier.GrowlNotifier(
applicationName=app_name, applicationName=app_name,
notifications=["Notification"], notifications=["Notification"],
applicationIcon=app_icon, applicationIcon=app_icon,
@ -50,7 +51,11 @@ class GNTPNotificationService(BaseNotificationService):
password=password, password=password,
port=port 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): def send_message(self, message="", **kwargs):
"""Send a message to a user.""" """Send a message to a user."""

View File

@ -2,7 +2,7 @@
Google Voice SMS platform for notify component. Google Voice SMS platform for notify component.
For more details about this platform, please refer to the documentation at 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 import logging

View File

@ -51,7 +51,7 @@ class SlackNotificationService(BaseNotificationService):
"""Send a message to a user.""" """Send a message to a user."""
import slacker import slacker
channel = kwargs.get('target', self._default_channel) channel = kwargs.get('target') or self._default_channel
try: try:
self.slack.chat.post_message(channel, message) self.slack.chat.post_message(channel, message)
except slacker.Error: except slacker.Error:

View File

@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-telegram-bot==4.0.1'] REQUIREMENTS = ['python-telegram-bot==4.1.1']
def get_service(hass, config): def get_service(hass, config):

View 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)

View 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

View File

@ -13,7 +13,8 @@ import logging
import queue import queue
import sqlite3 import sqlite3
import threading import threading
from datetime import date, datetime from datetime import date, datetime, timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
@ -21,6 +22,7 @@ from homeassistant.const import (
EVENT_TIME_CHANGED, MATCH_ALL) EVENT_TIME_CHANGED, MATCH_ALL)
from homeassistant.core import Event, EventOrigin, State from homeassistant.core import Event, EventOrigin, State
from homeassistant.remote import JSONEncoder from homeassistant.remote import JSONEncoder
from homeassistant.helpers.event import track_point_in_utc_time
DOMAIN = "recorder" DOMAIN = "recorder"
@ -30,6 +32,15 @@ RETURN_ROWCOUNT = "rowcount"
RETURN_LASTROWID = "lastrowid" RETURN_LASTROWID = "lastrowid"
RETURN_ONE_ROW = "one_row" 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 _INSTANCE = None
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -102,14 +113,14 @@ def setup(hass, config):
"""Setup the recorder.""" """Setup the recorder."""
# pylint: disable=global-statement # pylint: disable=global-statement
global _INSTANCE global _INSTANCE
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
_INSTANCE = Recorder(hass) _INSTANCE = Recorder(hass, purge_days=purge_days)
return True return True
class RecorderRun(object): class RecorderRun(object):
"""Representation of arecorder run.""" """Representation of a recorder run."""
def __init__(self, row=None): def __init__(self, row=None):
"""Initialize the recorder run.""" """Initialize the recorder run."""
@ -169,11 +180,12 @@ class Recorder(threading.Thread):
"""A threaded recorder class.""" """A threaded recorder class."""
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__(self, hass): def __init__(self, hass, purge_days):
"""Initialize the recorder.""" """Initialize the recorder."""
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.hass = hass self.hass = hass
self.purge_days = purge_days
self.conn = None self.conn = None
self.queue = queue.Queue() self.queue = queue.Queue()
self.quit_object = object() self.quit_object = object()
@ -194,6 +206,10 @@ class Recorder(threading.Thread):
"""Start processing events to save.""" """Start processing events to save."""
self._setup_connection() self._setup_connection()
self._setup_run() 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: while True:
event = self.queue.get() event = self.queue.get()
@ -475,6 +491,32 @@ class Recorder(threading.Thread):
"UPDATE recorder_runs SET end=? WHERE start=?", "UPDATE recorder_runs SET end=? WHERE start=?",
(dt_util.utcnow(), self.recording_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): def _adapt_datetime(datetimestamp):
"""Turn a datetime into an integer for in the DB.""" """Turn a datetime into an integer for in the DB."""

View File

@ -2,7 +2,7 @@
Support for Powerview scenes from a Powerview hub. Support for Powerview scenes from a Powerview hub.
For more details about this component, please refer to the documentation at 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 import logging

View File

@ -10,7 +10,7 @@ import logging
import datetime import datetime
import time import time
from homeassistant.const import HTTP_OK from homeassistant.const import HTTP_OK, TEMP_CELSIUS
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component from homeassistant.loader import get_component
@ -236,7 +236,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = [] dev = []
for resource in config.get("monitored_resources", for resource in config.get("monitored_resources",
FITBIT_DEFAULT_RESOURCE_LIST): 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) add_devices(dev)
else: else:
@ -314,8 +316,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class FitbitSensor(Entity): class FitbitSensor(Entity):
"""Implementation of a Fitbit sensor.""" """Implementation of a Fitbit sensor."""
def __init__(self, client, config_path, resource_type): def __init__(self, client, config_path, resource_type, is_metric):
"""Initialize the Uber sensor.""" """Initialize the Fitbit sensor."""
self.client = client self.client = client
self.config_path = config_path self.config_path = config_path
self.resource_type = resource_type self.resource_type = resource_type
@ -328,7 +330,13 @@ class FitbitSensor(Entity):
unit_type = FITBIT_RESOURCES_LIST[self.resource_type] unit_type = FITBIT_RESOURCES_LIST[self.resource_type]
if unit_type == "": if unit_type == "":
split_resource = self.resource_type.split("/") 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]] unit_type = measurement_system[split_resource[-1]]
self._unit_of_measurement = unit_type self._unit_of_measurement = unit_type
self._state = 0 self._state = 0

View File

@ -10,8 +10,10 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers.entity import Entity 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 from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -23,47 +25,102 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
CONF_ORIGIN = 'origin' CONF_ORIGIN = 'origin'
CONF_DESTINATION = 'destination' CONF_DESTINATION = 'destination'
CONF_TRAVEL_MODE = 'travel_mode' 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({ PLATFORM_SCHEMA = vol.Schema({
vol.Required('platform'): 'google_travel_time', vol.Required('platform'): 'google_travel_time',
vol.Optional(CONF_NAME): vol.Coerce(str),
vol.Required(CONF_API_KEY): vol.Coerce(str), vol.Required(CONF_API_KEY): vol.Coerce(str),
vol.Required(CONF_ORIGIN): vol.Coerce(str), vol.Required(CONF_ORIGIN): vol.Coerce(str),
vol.Required(CONF_DESTINATION): vol.Coerce(str), vol.Required(CONF_DESTINATION): vol.Coerce(str),
vol.Optional(CONF_TRAVEL_MODE, default='driving'): vol.Optional(CONF_TRAVEL_MODE):
vol.In(["driving", "walking", "bicycling", "transit"]) 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): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the travel time platform.""" """Setup the travel time platform."""
# pylint: disable=too-many-locals # 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) api_key = config.get(CONF_API_KEY)
origin = config.get(CONF_ORIGIN) origin = config.get(CONF_ORIGIN)
destination = config.get(CONF_DESTINATION) destination = config.get(CONF_DESTINATION)
travel_mode = config.get(CONF_TRAVEL_MODE)
sensor = GoogleTravelTimeSensor(api_key, origin, destination, sensor = GoogleTravelTimeSensor(name, api_key, origin, destination,
travel_mode, is_metric) options)
if sensor.valid_api_connection: if sensor.valid_api_connection:
add_devices_callback([sensor]) add_devices_callback([sensor])
# pylint: disable=too-many-instance-attributes
class GoogleTravelTimeSensor(Entity): class GoogleTravelTimeSensor(Entity):
"""Representation of a tavel time sensor.""" """Representation of a tavel time sensor."""
# pylint: disable=too-many-arguments # 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.""" """Initialize the sensor."""
if is_metric: self._name = name
self._unit = 'metric' self._options = options
else:
self._unit = 'imperial'
self._origin = origin self._origin = origin
self._destination = destination self._destination = destination
self._travel_mode = travel_mode
self._matrix = None self._matrix = None
self.valid_api_connection = True self.valid_api_connection = True
@ -79,17 +136,22 @@ class GoogleTravelTimeSensor(Entity):
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """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 @property
def name(self): def name(self):
"""Get the name of the sensor.""" """Get the name of the sensor."""
return "Google Travel time" return self._name
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
res = self._matrix.copy() res = self._matrix.copy()
res.update(self._options)
del res['rows'] del res['rows']
_data = self._matrix['rows'][0]['elements'][0] _data = self._matrix['rows'][0]['elements'][0]
if 'duration_in_traffic' in _data: if 'duration_in_traffic' in _data:
@ -108,10 +170,15 @@ class GoogleTravelTimeSensor(Entity):
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Get the latest data from Google.""" """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._matrix = self._client.distance_matrix(self._origin,
self._destination, self._destination,
mode=self._travel_mode, **options_copy)
units=self._unit,
departure_time=now,
traffic_model="optimistic")

View File

@ -7,14 +7,15 @@ https://home-assistant.io/components/sensor.gtfs/
import os import os
import logging import logging
import datetime import datetime
import threading
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["https://github.com/robbiet480/pygtfs/archive/" REQUIREMENTS = ["https://github.com/robbiet480/pygtfs/archive/"
"432414b720c580fb2667a0a48f539118a2d95969.zip#" "00546724e4bbcb3053110d844ca44e2246267dd8.zip#"
"pygtfs==0.1.2"] "pygtfs==0.1.3"]
ICON = "mdi:train" 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!") _LOGGER.error("The given GTFS data file/folder was not found!")
return False 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 = []
dev.append(GTFSDepartureSensor(config["data"], gtfs_dir, dev.append(GTFSDepartureSensor(gtfs, config["origin"],
config["origin"], config["destination"])) config["destination"]))
add_devices(dev) add_devices(dev)
# pylint: disable=too-many-instance-attributes,too-few-public-methods # 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): class GTFSDepartureSensor(Entity):
"""Implementation of an GTFS departures sensor.""" """Implementation of an GTFS departures sensor."""
def __init__(self, data_source, gtfs_folder, origin, destination): def __init__(self, pygtfs, origin, destination):
"""Initialize the sensor.""" """Initialize the sensor."""
self._data_source = data_source self._pygtfs = pygtfs
self._gtfs_folder = gtfs_folder
self.origin = origin self.origin = origin
self.destination = destination self.destination = destination
self._name = "GTFS Sensor" self._name = "GTFS Sensor"
self._unit_of_measurement = "min" self._unit_of_measurement = "min"
self._state = 0 self._state = 0
self._attributes = {} self._attributes = {}
self.lock = threading.Lock()
self.update() self.update()
@property @property
@ -202,62 +216,52 @@ class GTFSDepartureSensor(Entity):
def update(self): def update(self):
"""Get the latest data from GTFS and update the states.""" """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]) name = "{} {} to {} next departure"
gtfs = pygtfs.Schedule(os.path.join(self._gtfs_folder, sqlite_file)) self._name = name.format(agency.agency_name,
origin_station.stop_id,
destination_station.stop_id)
# pylint: disable=no-member # Build attributes
if len(gtfs.feeds) < 1:
pygtfs.append_feed(gtfs, os.path.join(self._gtfs_folder,
self._data_source))
self._departure = get_next_departure(gtfs, self.origin, self._attributes = {}
self.destination)
self._state = self._departure["minutes_until_departure"]
origin_station = self._departure["origin_station"] def dict_for_table(resource):
destination_station = self._departure["destination_station"] """Return a dict for the SQLAlchemy resource given."""
origin_stop_time = self._departure["origin_stop_time"] return dict((col, getattr(resource, col))
destination_stop_time = self._departure["destination_stop_time"] for col in resource.__table__.columns.keys())
agency = self._departure["agency"]
route = self._departure["route"]
trip = self._departure["trip"]
name = "{} {} to {} next departure" def append_keys(resource, prefix=None):
self._name = name.format(agency.agency_name, """Properly format key val pairs to append to attributes."""
origin_station.stop_id, for key, val in resource.items():
destination_station.stop_id) 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 append_keys(dict_for_table(agency), "Agency")
append_keys(dict_for_table(route), "Route")
self._attributes = {} append_keys(dict_for_table(trip), "Trip")
append_keys(dict_for_table(origin_station), "Origin Station")
def dict_for_table(resource): append_keys(dict_for_table(destination_station),
"""Return a dict for the SQLAlchemy resource given.""" "Destination Station")
return dict((col, getattr(resource, col)) append_keys(origin_stop_time, "Origin Stop")
for col in resource.__table__.columns.keys()) append_keys(destination_stop_time, "Destination Stop")
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")

View 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

View File

@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "loopenergy" DOMAIN = "loopenergy"
REQUIREMENTS = ['pyloopenergy==0.0.10'] REQUIREMENTS = ['pyloopenergy==0.0.12']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):

View 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),
}

View File

@ -4,6 +4,8 @@ Support for Nest Thermostat Sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.nest/ https://home-assistant.io/components/sensor.nest/
""" """
from itertools import chain
import voluptuous as vol import voluptuous as vol
import homeassistant.components.nest as nest import homeassistant.components.nest as nest
@ -29,9 +31,13 @@ WEATHER_VARS = {'weather_humidity': 'humidity',
SENSOR_UNITS = {'humidity': '%', 'battery_level': 'V', SENSOR_UNITS = {'humidity': '%', 'battery_level': 'V',
'kph': 'kph', 'temperature': '°C'} 'kph': 'kph', 'temperature': '°C'}
PROTECT_VARS = ['co_status',
'smoke_status',
'battery_level']
SENSOR_TEMP_TYPES = ['temperature', 'target'] 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()) list(WEATHER_VARS.keys())
PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = vol.Schema({
@ -44,20 +50,34 @@ PLATFORM_SCHEMA = vol.Schema({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Nest Sensor.""" """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) sensors = [NestBasicSensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS] 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) sensors += [NestTempSensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS] 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, sensors += [NestWeatherSensor(structure, device,
WEATHER_VARS[variable]) WEATHER_VARS[variable])
for variable in config[CONF_MONITORED_CONDITIONS] 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) 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): class NestSensor(Entity):
"""Representation of a Nest sensor.""" """Representation of a Nest sensor."""
@ -130,3 +150,28 @@ class NestWeatherSensor(NestSensor):
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
return SENSOR_UNITS.get(self.variable, None) 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)

View File

@ -7,11 +7,12 @@ https://home-assistant.io/components/sensor.speedtest/
import logging import logging
import re import re
import sys import sys
from datetime import timedelta
from subprocess import check_output 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.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.helpers.event import track_time_change
REQUIREMENTS = ['speedtest-cli==0.3.4'] REQUIREMENTS = ['speedtest-cli==0.3.4']
_LOGGER = logging.getLogger(__name__) _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]+') r'Upload:\s(\d+\.\d+)\sMbit/s[\r\n]+')
CONF_MONITORED_CONDITIONS = 'monitored_conditions' CONF_MONITORED_CONDITIONS = 'monitored_conditions'
CONF_SECOND = 'second'
CONF_MINUTE = 'minute' CONF_MINUTE = 'minute'
CONF_HOUR = 'hour' CONF_HOUR = 'hour'
CONF_DAY = 'day' CONF_DAY = 'day'
@ -30,13 +32,10 @@ SENSOR_TYPES = {
'upload': ['Upload', 'Mbit/s'], '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): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Speedtest sensor.""" """Setup the Speedtest sensor."""
data = SpeedtestData() data = SpeedtestData(hass, config)
dev = [] dev = []
for sensor in config[CONF_MONITORED_CONDITIONS]: for sensor in config[CONF_MONITORED_CONDITIONS]:
if sensor not in SENSOR_TYPES: if sensor not in SENSOR_TYPES:
@ -46,6 +45,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(dev) 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 # pylint: disable=too-few-public-methods
class SpeedtestSensor(Entity): class SpeedtestSensor(Entity):
@ -76,7 +83,6 @@ class SpeedtestSensor(Entity):
def update(self): def update(self):
"""Get the latest data and update the states.""" """Get the latest data and update the states."""
self.speedtest_client.update()
data = self.speedtest_client.data data = self.speedtest_client.data
if data is None: if data is None:
return return
@ -92,12 +98,16 @@ class SpeedtestSensor(Entity):
class SpeedtestData(object): class SpeedtestData(object):
"""Get the latest data from speedtest.net.""" """Get the latest data from speedtest.net."""
def __init__(self): def __init__(self, hass, config):
"""Initialize the data object.""" """Initialize the data object."""
self.data = None 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, now):
def update(self):
"""Get the latest data from speedtest.net.""" """Get the latest data from speedtest.net."""
import speedtest_cli import speedtest_cli

View 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
View File

@ -10,7 +10,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['psutil==4.1.0'] REQUIREMENTS = ['psutil==4.2.0']
SENSOR_TYPES = { SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'], 'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'],
'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'], 'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'],
@ -24,9 +24,9 @@ SENSOR_TYPES = {
'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'], 'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'],
'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'], 'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'],
'network_out': ['Sent', 'MiB', 'mdi:server-network'], '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_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'], 'ipv4_address': ['IPv4 address', '', 'mdi:server-network'],
'ipv6_address': ['IPv6 address', '', 'mdi:server-network'], 'ipv6_address': ['IPv6 address', '', 'mdi:server-network'],
'last_boot': ['Last Boot', '', 'mdi:clock'], 'last_boot': ['Last Boot', '', 'mdi:clock'],

View 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

View File

@ -14,7 +14,7 @@ REQUIREMENTS = ['rpi-rf==0.9.5']
_LOGGER = logging.getLogger(__name__) _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): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Find and return switches controlled by a generic RF device via GPIO.""" """Find and return switches controlled by a generic RF device via GPIO."""
import rpi_rf import rpi_rf

View File

@ -40,7 +40,6 @@ class Thermostat(ThermostatDevice):
self.thermostat = self.data.ecobee.get_thermostat( self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index) self.thermostat_index)
self._name = self.thermostat['name'] self._name = self.thermostat['name']
self._away = 'away' in self.thermostat['program']['currentClimateRef']
self.hold_temp = hold_temp self.hold_temp = hold_temp
def update(self): def update(self):
@ -121,9 +120,7 @@ class Thermostat(ThermostatDevice):
@property @property
def mode(self): def mode(self):
"""Return current mode ie. home, away, sleep.""" """Return current mode ie. home, away, sleep."""
mode = self.thermostat['program']['currentClimateRef'] return self.thermostat['program']['currentClimateRef']
self._away = 'away' in mode
return mode
@property @property
def hvac_mode(self): def hvac_mode(self):
@ -144,11 +141,16 @@ class Thermostat(ThermostatDevice):
@property @property
def is_away_mode_on(self): def is_away_mode_on(self):
"""Return true if away mode is on.""" """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): def turn_away_mode_on(self):
"""Turn away on.""" """Turn away on."""
self._away = True
if self.hold_temp: if self.hold_temp:
self.data.ecobee.set_climate_hold(self.thermostat_index, self.data.ecobee.set_climate_hold(self.thermostat_index,
"away", "indefinite") "away", "indefinite")
@ -157,7 +159,6 @@ class Thermostat(ThermostatDevice):
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Turn away off.""" """Turn away off."""
self._away = False
self.data.ecobee.resume_program(self.thermostat_index) self.data.ecobee.resume_program(self.thermostat_index)
def set_temperature(self, temperature): def set_temperature(self, temperature):
@ -180,20 +181,16 @@ class Thermostat(ThermostatDevice):
# def turn_home_mode_on(self): # def turn_home_mode_on(self):
# """ Turns home mode on. """ # """ Turns home mode on. """
# self._away = False
# self.data.ecobee.set_climate_hold(self.thermostat_index, "home") # self.data.ecobee.set_climate_hold(self.thermostat_index, "home")
# def turn_home_mode_off(self): # def turn_home_mode_off(self):
# """ Turns home mode off. """ # """ Turns home mode off. """
# self._away = False
# self.data.ecobee.resume_program(self.thermostat_index) # self.data.ecobee.resume_program(self.thermostat_index)
# def turn_sleep_mode_on(self): # def turn_sleep_mode_on(self):
# """ Turns sleep mode on. """ # """ Turns sleep mode on. """
# self._away = False
# self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep") # self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep")
# def turn_sleep_mode_off(self): # def turn_sleep_mode_off(self):
# """ Turns sleep mode off. """ # """ Turns sleep mode off. """
# self._away = False
# self.data.ecobee.resume_program(self.thermostat_index) # self.data.ecobee.resume_program(self.thermostat_index)

View File

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
__version__ = "0.19.4" __version__ = "0.20.0.dev0"
REQUIRED_PYTHON_VER = (3, 4) REQUIRED_PYTHON_VER = (3, 4)
PLATFORM_FORMAT = '{}.{}' PLATFORM_FORMAT = '{}.{}'

View File

@ -79,6 +79,7 @@ class HomeAssistant(object):
def restart_homeassistant(*args): def restart_homeassistant(*args):
"""Reset Home Assistant.""" """Reset Home Assistant."""
_LOGGER.warning('Home Assistant requested a restart.')
request_restart.set() request_restart.set()
request_shutdown.set() request_shutdown.set()
@ -92,14 +93,21 @@ class HomeAssistant(object):
except ValueError: except ValueError:
_LOGGER.warning( _LOGGER.warning(
'Could not bind to SIGTERM. Are you running in a thread?') 'Could not bind to SIGTERM. Are you running in a thread?')
try:
while not request_shutdown.isSet(): signal.signal(signal.SIGHUP, restart_homeassistant)
try: 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) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
break pass
finally:
self.stop()
self.stop()
return RESTART_EXIT_CODE if request_restart.isSet() else 0 return RESTART_EXIT_CODE if request_restart.isSet() else 0
def stop(self): def stop(self):

View File

@ -55,12 +55,23 @@ class EntityComponent(object):
self._setup_platform(p_type, p_config) self._setup_platform(p_type, p_config)
if self.discovery_platforms: 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( discovery.listen(
self.hass, self.discovery_platforms.keys(), self.hass, self.discovery_platforms.keys(),
lambda service, info: lambda service, info:
self._setup_platform(self.discovery_platforms[service], {}, self._setup_platform(self.discovery_platforms[service], {},
info)) 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): def extract_from_service(self, service):
"""Extract all known entities from a service call. """Extract all known entities from a service call.

View File

@ -16,8 +16,8 @@ from homeassistant.components.thermostat import (
ATTR_AWAY_MODE, ATTR_FAN, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE, ATTR_AWAY_MODE, ATTR_FAN, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE,
SERVICE_SET_TEMPERATURE) SERVICE_SET_TEMPERATURE)
from homeassistant.components.hvac import ( from homeassistant.components.hvac import (
ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION, ATTR_AUX_HEAT, ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION_MODE, ATTR_AUX_HEAT,
SERVICE_SET_HUMIDITY, SERVICE_SET_SWING, SERVICE_SET_HUMIDITY, SERVICE_SET_SWING_MODE,
SERVICE_SET_OPERATION_MODE, SERVICE_SET_AUX_HEAT) SERVICE_SET_OPERATION_MODE, SERVICE_SET_AUX_HEAT)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY,
@ -48,8 +48,8 @@ SERVICE_ATTRIBUTES = {
SERVICE_SET_FAN_MODE: [ATTR_FAN], SERVICE_SET_FAN_MODE: [ATTR_FAN],
SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE], SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE],
SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY], SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY],
SERVICE_SET_SWING: [ATTR_SWING_MODE], SERVICE_SET_SWING_MODE: [ATTR_SWING_MODE],
SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION], SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE],
SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT], SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT],
SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE],
} }

View File

@ -57,6 +57,7 @@ def render(hass, template, variables=None, **kwargs):
'states': AllStates(hass), 'states': AllStates(hass),
'utcnow': utcnow, 'utcnow': utcnow,
'as_timestamp': dt_util.as_timestamp, 'as_timestamp': dt_util.as_timestamp,
'relative_time': dt_util.get_age
}).render(kwargs).strip() }).render(kwargs).strip()
except jinja2.TemplateError as err: except jinja2.TemplateError as err:
raise TemplateError(err) raise TemplateError(err)

View File

@ -1,48 +1,47 @@
"""Color util methods.""" """Color util methods."""
import math 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_MAX = 500 # mireds (inverted)
HASS_COLOR_MIN = 154 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. # License: Code is given as is. Use at your own risk and discretion.
# pylint: disable=invalid-name # pylint: disable=invalid-name
def color_RGB_to_xy(R, G, B): def color_RGB_to_xy(R, G, B):
"""Convert from RGB color to XY color.""" """Convert from RGB color to XY color."""
if R + G + B == 0: if R + G + B == 0:
return 0, 0 return 0, 0, 0
var_R = (R / 255.) R = R / 255
var_G = (G / 255.) B = B / 255
var_B = (B / 255.) G = G / 255
if var_R > 0.04045: # Gamma correction
var_R = ((var_R + 0.055) / 1.055) ** 2.4 R = pow((R + 0.055) / (1.0 + 0.055),
else: 2.4) if (R > 0.04045) else (R / 12.92)
var_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: # Wide RGB D65 conversion formula
var_G = ((var_G + 0.055) / 1.055) ** 2.4 X = R * 0.664511 + G * 0.154324 + B * 0.162028
else: Y = R * 0.313881 + G * 0.668433 + B * 0.047685
var_G /= 12.92 Z = R * 0.000088 + G * 0.072310 + B * 0.986039
if var_B > 0.04045: # Convert XYZ to xy
var_B = ((var_B + 0.055) / 1.055) ** 2.4 x = X / (X + Y + Z)
else: y = Y / (X + Y + Z)
var_B /= 12.92
var_R *= 100 # Brightness
var_G *= 100 Y = 1 if Y > 1 else Y
var_B *= 100 brightness = round(Y * 255)
# Observer. = 2 deg, Illuminant = D65 return round(x, 3), round(y, 3), brightness
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)
# taken from # taken from

View File

@ -152,3 +152,52 @@ def parse_time(time_str):
except ValueError: except ValueError:
# ValueError if value cannot be converted to an int or not in range # ValueError if value cannot be converted to an int or not in range
return None 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"

View File

@ -3,6 +3,7 @@ import logging
import os import os
from collections import OrderedDict from collections import OrderedDict
import glob
import yaml import yaml
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -44,6 +45,44 @@ def _include_yaml(loader, node):
return load_yaml(fname) 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): def _ordered_dict(loader, node):
"""Load YAML mappings into an ordered dict to preserve key order.""" """Load YAML mappings into an ordered dict to preserve key order."""
loader.flatten_mapping(node) 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, yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
_ordered_dict) _ordered_dict)
yaml.SafeLoader.add_constructor('!env_var', _env_var_yaml) 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)

View File

@ -6,6 +6,7 @@ pip>=7.0.0
vincenty==0.1.4 vincenty==0.1.4
jinja2>=2.8 jinja2>=2.8
voluptuous==0.8.9 voluptuous==0.8.9
webcolors==1.5
# homeassistant.components.isy994 # homeassistant.components.isy994
PyISY==1.0.5 PyISY==1.0.5
@ -37,6 +38,11 @@ blockchain==1.3.1
# homeassistant.components.thermostat.eq3btsmart # homeassistant.components.thermostat.eq3btsmart
# bluepy_devices>=0.2.0 # 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 # homeassistant.components.notify.xmpp
dnspython3==1.12.0 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 # homeassistant.components.alarm_control_panel.alarmdotcom
https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 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 # homeassistant.components.modbus
https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 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 # homeassistant.components.sensor.sabnzbd
https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1 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 # 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 # homeassistant.components.switch.edimax
https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1 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 https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3
# homeassistant.components.sensor.gtfs # 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 # homeassistant.components.scene.hunterdouglas_powerview
https://github.com/sander76/powerviewApi/archive/master.zip#powerviewApi==0.2 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 # homeassistant.components.notify.googlevoice
https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#pygooglevoice-sms==0.0.1 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 # homeassistant.components.influxdb
influxdb==2.12.0 influxdb==2.12.0
@ -153,7 +168,7 @@ messagebird==1.2.0
mficlient==0.3.0 mficlient==0.3.0
# homeassistant.components.discovery # homeassistant.components.discovery
netdisco==0.6.6 netdisco==0.6.7
# homeassistant.components.sensor.neurio_energy # homeassistant.components.sensor.neurio_energy
neurio==0.2.10 neurio==0.2.10
@ -168,6 +183,7 @@ paho-mqtt==1.1
panasonic_viera==0.2 panasonic_viera==0.2
# homeassistant.components.device_tracker.aruba # homeassistant.components.device_tracker.aruba
# homeassistant.components.device_tracker.asuswrt
pexpect==4.0.1 pexpect==4.0.1
# homeassistant.components.light.hue # homeassistant.components.light.hue
@ -180,7 +196,7 @@ plexapi==1.1.0
proliphix==0.1.0 proliphix==0.1.0
# homeassistant.components.sensor.systemmonitor # homeassistant.components.sensor.systemmonitor
psutil==4.1.0 psutil==4.2.0
# homeassistant.components.notify.pushbullet # homeassistant.components.notify.pushbullet
pushbullet.py==0.10.0 pushbullet.py==0.10.0
@ -215,8 +231,11 @@ pyfttt==0.3
# homeassistant.components.device_tracker.icloud # homeassistant.components.device_tracker.icloud
pyicloud==0.8.3 pyicloud==0.8.3
# homeassistant.components.sensor.lastfm
pylast==1.6.0
# homeassistant.components.sensor.loopenergy # homeassistant.components.sensor.loopenergy
pyloopenergy==0.0.10 pyloopenergy==0.0.12
# homeassistant.components.device_tracker.netgear # homeassistant.components.device_tracker.netgear
pynetgear==0.3.3 pynetgear==0.3.3
@ -241,7 +260,7 @@ python-forecastio==1.3.4
python-mpd2==0.5.5 python-mpd2==0.5.5
# homeassistant.components.nest # homeassistant.components.nest
python-nest==2.6.0 python-nest==2.9.2
# homeassistant.components.device_tracker.nmap_tracker # homeassistant.components.device_tracker.nmap_tracker
python-nmap==0.6.0 python-nmap==0.6.0
@ -253,7 +272,7 @@ python-pushover==0.2
python-statsd==1.7.2 python-statsd==1.7.2
# homeassistant.components.notify.telegram # homeassistant.components.notify.telegram
python-telegram-bot==4.0.1 python-telegram-bot==4.1.1
# homeassistant.components.sensor.twitch # homeassistant.components.sensor.twitch
python-twitch==1.2.0 python-twitch==1.2.0
@ -280,7 +299,7 @@ pywemo==0.4.2
radiotherm==1.2 radiotherm==1.2
# homeassistant.components.switch.rpi_rf # homeassistant.components.switch.rpi_rf
rpi-rf==0.9.5 # rpi-rf==0.9.5
# homeassistant.components.media_player.yamaha # homeassistant.components.media_player.yamaha
rxv==0.1.11 rxv==0.1.11
@ -326,6 +345,9 @@ tellive-py==0.5.2
# homeassistant.components.switch.transmission # homeassistant.components.switch.transmission
transmissionrpc==0.11 transmissionrpc==0.11
# homeassistant.components.notify.twilio_sms
twilio==5.4.0
# homeassistant.components.sensor.uber # homeassistant.components.sensor.uber
uber_rides==0.2.1 uber_rides==0.2.1
@ -344,6 +366,9 @@ vsure==0.8.1
# homeassistant.components.switch.wake_on_lan # homeassistant.components.switch.wake_on_lan
wakeonlan==0.2.2 wakeonlan==0.2.2
# homeassistant.components.media_player.gpmdp
websocket-client==0.35.0
# homeassistant.components.zigbee # homeassistant.components.zigbee
xbee-helper==0.0.7 xbee-helper==0.0.7

View File

@ -1,4 +1,4 @@
flake8>=2.5.1 flake8>=2.5.4
pylint>=1.5.5 pylint>=1.5.5
coveralls>=1.1 coveralls>=1.1
pytest>=2.9.1 pytest>=2.9.1

View File

@ -15,7 +15,7 @@ if [ -d python-openzwave ]; then
git pull --recurse-submodules=yes git pull --recurse-submodules=yes
git submodule update --init --recursive git submodule update --init --recursive
else 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 cd python-openzwave
fi fi

View File

@ -8,6 +8,7 @@ import sys
COMMENT_REQUIREMENTS = [ COMMENT_REQUIREMENTS = [
'RPi.GPIO', 'RPi.GPIO',
'rpi-rf',
'Adafruit_Python_DHT', 'Adafruit_Python_DHT',
'fritzconnection', 'fritzconnection',
'pybluez', 'pybluez',

View File

@ -12,8 +12,9 @@ User=%i
# Enable the following line if you get network-related HA errors during boot # Enable the following line if you get network-related HA errors during boot
#ExecStartPre=/usr/bin/sleep 60 #ExecStartPre=/usr/bin/sleep 60
# Use `whereis hass` to determine the path of hass # Use `whereis hass` to determine the path of hass
ExecStart=/usr/bin/hass ExecStart=/usr/bin/hass --runner
SendSIGKILL=no SendSIGKILL=no
RestartForceExitStatus=100
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -18,6 +18,7 @@ REQUIRES = [
'vincenty==0.1.4', 'vincenty==0.1.4',
'jinja2>=2.8', 'jinja2>=2.8',
'voluptuous==0.8.9', 'voluptuous==0.8.9',
'webcolors==1.5',
] ]
setup( setup(

View File

@ -55,6 +55,21 @@ LOCATION_MESSAGE_INACCURATE = {
'tst': 1, 'tst': 1,
'vel': 0} '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 = { REGION_ENTER_MESSAGE = {
'lon': 1.0, 'lon': 1.0,
'event': 'enter', 'event': 'enter',
@ -204,6 +219,14 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase):
self.assert_location_latitude(2.0) self.assert_location_latitude(2.0)
self.assert_location_longitude(1.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): def test_event_entry_exit(self):
"""Test the entry event.""" """Test the entry event."""
self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE) self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)
@ -230,6 +253,20 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase):
# Left clean zone state # Left clean zone state
self.assertFalse(owntracks.REGIONS_ENTERED[USER]) 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): def test_event_entry_exit_inaccurate(self):
"""Test the event for inaccurate exit.""" """Test the event for inaccurate exit."""
self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE) self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)

View File

@ -33,7 +33,7 @@ class TestDemoHvac(unittest.TestCase):
self.assertEqual(21, state.attributes.get('temperature')) self.assertEqual(21, state.attributes.get('temperature'))
self.assertEqual('on', state.attributes.get('away_mode')) self.assertEqual('on', state.attributes.get('away_mode'))
self.assertEqual(22, state.attributes.get('current_temperature')) 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(67, state.attributes.get('humidity'))
self.assertEqual(54, state.attributes.get('current_humidity')) self.assertEqual(54, state.attributes.get('current_humidity'))
self.assertEqual("Off", state.attributes.get('swing_mode')) self.assertEqual("Off", state.attributes.get('swing_mode'))
@ -81,17 +81,17 @@ class TestDemoHvac(unittest.TestCase):
def test_set_fan_mode_bad_attr(self): def test_set_fan_mode_bad_attr(self):
"""Test setting fan mode without required attribute.""" """Test setting fan mode without required attribute."""
state = self.hass.states.get(ENTITY_HVAC) 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) hvac.set_fan_mode(self.hass, None, ENTITY_HVAC)
self.hass.pool.block_till_done() 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): def test_set_fan_mode(self):
"""Test setting of new fan mode.""" """Test setting of new fan mode."""
hvac.set_fan_mode(self.hass, "On Low", ENTITY_HVAC) hvac.set_fan_mode(self.hass, "On Low", ENTITY_HVAC)
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
state = self.hass.states.get(ENTITY_HVAC) 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): def test_set_swing_mode_bad_attr(self):
"""Test setting swing mode without required attribute.""" """Test setting swing mode without required attribute."""

View 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'

View 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()

View File

@ -1,6 +1,8 @@
"""The tests for the Recorder component.""" """The tests for the Recorder component."""
# pylint: disable=too-many-public-methods,protected-access # pylint: disable=too-many-public-methods,protected-access
import unittest import unittest
import time
import json
from unittest.mock import patch from unittest.mock import patch
from homeassistant.const import MATCH_ALL from homeassistant.const import MATCH_ALL
@ -10,7 +12,7 @@ from tests.common import get_test_home_assistant
class TestRecorder(unittest.TestCase): class TestRecorder(unittest.TestCase):
"""Test the chromecast module.""" """Test the recorder module."""
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
@ -25,6 +27,52 @@ class TestRecorder(unittest.TestCase):
self.hass.stop() self.hass.stop()
recorder._INSTANCE.block_till_done() 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): def test_saving_state(self):
"""Test saving and restoring a state.""" """Test saving and restoring a state."""
entity_id = 'test.recorder' entity_id = 'test.recorder'
@ -76,3 +124,56 @@ class TestRecorder(unittest.TestCase):
# Recorder uses SQLite and stores datetimes as integer unix timestamps # Recorder uses SQLite and stores datetimes as integer unix timestamps
assert event.time_fired.replace(microsecond=0) == \ assert event.time_fired.replace(microsecond=0) == \
db_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)

View File

@ -37,10 +37,10 @@ class TestRFXTRX(unittest.TestCase):
'automatic_add': True, 'automatic_add': True,
'devices': {}}})) 'devices': {}}}))
while len(rfxtrx.RFX_DEVICES) < 1: while len(rfxtrx.RFX_DEVICES) < 2:
time.sleep(0.1) time.sleep(0.1)
self.assertEqual(len(rfxtrx.RFXOBJECT.sensors()), 1) self.assertEqual(len(rfxtrx.RFXOBJECT.sensors()), 2)
def test_valid_config(self): def test_valid_config(self):
"""Test configuration.""" """Test configuration."""

View File

@ -9,16 +9,17 @@ class TestColorUtil(unittest.TestCase):
# pylint: disable=invalid-name # pylint: disable=invalid-name
def test_color_RGB_to_xy(self): def test_color_RGB_to_xy(self):
"""Test color_RGB_to_xy.""" """Test color_RGB_to_xy."""
self.assertEqual((0, 0), color_util.color_RGB_to_xy(0, 0, 0)) self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy(0, 0, 0))
self.assertEqual((0.3127159072215825, 0.3290014805066623), self.assertEqual((0.32, 0.336, 255),
color_util.color_RGB_to_xy(255, 255, 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)) 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)) color_util.color_RGB_to_xy(255, 0, 0))
def test_color_xy_brightness_to_RGB(self): def test_color_xy_brightness_to_RGB(self):

View File

@ -133,3 +133,32 @@ class TestDateUtil(unittest.TestCase):
def test_parse_datetime_returns_none_for_incorrect_format(self): def test_parse_datetime_returns_none_for_incorrect_format(self):
"""Test parse_datetime returns None if incorrect format.""" """Test parse_datetime returns None if incorrect format."""
self.assertIsNone(dt_util.parse_datetime("not a datetime string")) 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")

View File

@ -1,6 +1,8 @@
"""Test Home Assistant yaml loader.""" """Test Home Assistant yaml loader."""
import io import io
import unittest import unittest
import os
import tempfile
from homeassistant.util import yaml from homeassistant.util import yaml
@ -32,3 +34,104 @@ class TestYaml(unittest.TestCase):
pass pass
else: else:
assert 0 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"
}