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

View File

@ -3,6 +3,8 @@
**Related issue (if applicable):** #
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
**Example entry for `configuration.yaml` (if applicable):**
```yaml
@ -10,6 +12,9 @@
**Checklist:**
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
If code communicates with devices:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).

2
.gitignore vendored
View File

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

View File

@ -21,6 +21,14 @@ RUN script/build_python_openzwave && \
COPY requirements_all.txt requirements_all.txt
RUN pip3 install --no-cache-dir -r requirements_all.txt
RUN wget http://www.openssl.org/source/openssl-1.0.2h.tar.gz && \
tar -xvzf openssl-1.0.2h.tar.gz && \
cd openssl-1.0.2h && \
./config --prefix=/usr/ && \
make && \
make install && \
rm -rf openssl-1.0.2h*
# Copy source
COPY . .

View File

@ -3,11 +3,12 @@ from __future__ import print_function
import argparse
import os
import platform
import signal
import subprocess
import sys
import threading
import time
from multiprocessing import Process
from homeassistant.const import (
__version__,
@ -87,8 +88,7 @@ def get_arguments():
parser.add_argument(
'--debug',
action='store_true',
help='Start Home Assistant in debug mode. Runs in single process to '
'enable use of interactive debuggers.')
help='Start Home Assistant in debug mode')
parser.add_argument(
'--open-ui',
action='store_true',
@ -123,15 +123,20 @@ def get_arguments():
'--restart-osx',
action='store_true',
help='Restarts on OS X.')
if os.name != "nt":
parser.add_argument(
'--runner',
action='store_true',
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
if os.name == "posix":
parser.add_argument(
'--daemon',
action='store_true',
help='Run Home Assistant as daemon')
arguments = parser.parse_args()
if os.name == "nt":
if os.name != "posix" or arguments.debug or arguments.runner:
arguments.daemon = False
return arguments
@ -144,13 +149,21 @@ def daemonize():
# Decouple fork
os.setsid()
os.umask(0)
# Create second fork
pid = os.fork()
if pid > 0:
sys.exit(0)
# redirect standard file descriptors to devnull
infd = open(os.devnull, 'r')
outfd = open(os.devnull, 'a+')
sys.stdout.flush()
sys.stderr.flush()
os.dup2(infd.fileno(), sys.stdin.fileno())
os.dup2(outfd.fileno(), sys.stdout.fileno())
os.dup2(outfd.fileno(), sys.stderr.fileno())
def check_pid(pid_file):
"""Check that HA is not already running."""
@ -161,6 +174,10 @@ def check_pid(pid_file):
# PID File does not exist
return
# If we just restarted, we just found our own pidfile.
if pid == os.getpid():
return
try:
os.kill(pid, 0)
except OSError:
@ -220,29 +237,61 @@ def uninstall_osx():
print("Home Assistant has been uninstalled.")
def setup_and_run_hass(config_dir, args, top_process=False):
"""Setup HASS and run.
def closefds_osx(min_fd, max_fd):
"""Make sure file descriptors get closed when we restart.
Block until stopped. Will assume it is running in a subprocess unless
top_process is set to true.
We cannot call close on guarded fds, and we cannot easily test which fds
are guarded. But we can set the close-on-exec flag on everything we want to
get rid of.
"""
from fcntl import fcntl, F_GETFD, F_SETFD, FD_CLOEXEC
for _fd in range(min_fd, max_fd):
try:
val = fcntl(_fd, F_GETFD)
if not val & FD_CLOEXEC:
fcntl(_fd, F_SETFD, val | FD_CLOEXEC)
except IOError:
pass
def cmdline():
"""Collect path and arguments to re-execute the current hass instance."""
if sys.argv[0].endswith('/__main__.py'):
modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
def setup_and_run_hass(config_dir, args):
"""Setup HASS and run."""
from homeassistant import bootstrap
# Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv:
args = cmdline() + ['--runner']
while True:
try:
subprocess.check_call(args)
sys.exit(0)
except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE:
sys.exit(exc.returncode)
if args.demo_mode:
config = {
'frontend': {},
'demo': {}
}
hass = bootstrap.from_config_dict(
config, config_dir=config_dir, daemon=args.daemon,
verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days)
config, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
else:
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
hass = bootstrap.from_config_file(
config_file, daemon=args.daemon, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days)
if hass is None:
return
@ -256,42 +305,68 @@ def setup_and_run_hass(config_dir, args, top_process=False):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
print('Starting Home-Assistant')
hass.start()
exit_code = int(hass.block_till_stopped())
if not top_process:
sys.exit(exit_code)
return exit_code
def run_hass_process(hass_proc):
"""Run a child hass process. Returns True if it should be restarted."""
requested_stop = threading.Event()
hass_proc.daemon = True
def try_to_restart():
"""Attempt to clean up state and start a new homeassistant instance."""
# Things should be mostly shut down already at this point, now just try
# to clean up things that may have been left behind.
sys.stderr.write('Home Assistant attempting to restart.\n')
def request_stop(*args):
"""Request hass stop, *args is for signal handler callback."""
requested_stop.set()
hass_proc.terminate()
# Count remaining threads, ideally there should only be one non-daemonized
# thread left (which is us). Nothing we really do with it, but it might be
# useful when debugging shutdown/restart issues.
nthreads = sum(thread.isAlive() and not thread.isDaemon()
for thread in threading.enumerate())
if nthreads > 1:
sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads))
try:
signal.signal(signal.SIGTERM, request_stop)
except ValueError:
print('Could not bind to SIGTERM. Are you running in a thread?')
# Send terminate signal to all processes in our process group which
# should be any children that have not themselves changed the process
# group id. Don't bother if couldn't even call setpgid.
if hasattr(os, 'setpgid'):
sys.stderr.write("Signalling child processes to terminate...\n")
os.kill(0, signal.SIGTERM)
hass_proc.start()
try:
hass_proc.join()
except KeyboardInterrupt:
request_stop()
# wait for child processes to terminate
try:
hass_proc.join()
except KeyboardInterrupt:
return False
while True:
time.sleep(1)
if os.waitpid(0, os.WNOHANG) == (0, 0):
break
except OSError:
pass
return (not requested_stop.isSet() and
hass_proc.exitcode == RESTART_EXIT_CODE,
hass_proc.exitcode)
elif os.name == 'nt':
# Maybe one of the following will work, but how do we indicate which
# processes are our children if there is no process group?
# os.kill(0, signal.CTRL_C_EVENT)
# os.kill(0, signal.CTRL_BREAK_EVENT)
pass
# Try to not leave behind open filedescriptors with the emphasis on try.
try:
max_fd = os.sysconf("SC_OPEN_MAX")
except ValueError:
max_fd = 256
if platform.system() == 'Darwin':
closefds_osx(3, max_fd)
else:
os.closerange(3, max_fd)
# Now launch into a new instance of Home-Assistant. If this fails we
# fall through and exit with error 100 (RESTART_EXIT_CODE) in which case
# systemd will restart us when RestartForceExitStatus=100 is set in the
# systemd.service file.
sys.stderr.write("Restarting Home-Assistant\n")
args = cmdline()
os.execv(args[0], args)
def main():
@ -325,21 +400,17 @@ def main():
if args.pid_file:
write_pid(args.pid_file)
# Run hass in debug mode if requested
if args.debug:
sys.stderr.write('Running in debug mode. '
'Home Assistant will not be able to restart.\n')
exit_code = setup_and_run_hass(config_dir, args, top_process=True)
if exit_code == RESTART_EXIT_CODE:
sys.stderr.write('Home Assistant requested a '
'restart in debug mode.\n')
return exit_code
# Create new process group if we can
if hasattr(os, 'setpgid'):
try:
os.setpgid(0, 0)
except PermissionError:
pass
exit_code = setup_and_run_hass(config_dir, args)
if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart()
# Run hass as child process. Restart if necessary.
keep_running = True
while keep_running:
hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args))
keep_running, exit_code = run_hass_process(hass_proc)
return exit_code

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,10 +15,12 @@ from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED)
DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.6.6']
REQUIREMENTS = ['netdisco==0.6.7']
SCAN_INTERVAL = 300 # seconds
LOAD_PLATFORM = 'load_platform'
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HUE = 'philips_hue'
SERVICE_CAST = 'google_cast'
@ -27,6 +29,7 @@ SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver'
SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
SERVICE_PANASONIC_VIERA = 'panasonic_viera'
SERVICE_ROKU = 'roku'
SERVICE_HANDLERS = {
SERVICE_WEMO: "wemo",
@ -37,6 +40,7 @@ SERVICE_HANDLERS = {
SERVICE_PLEX: 'media_player',
SERVICE_SQUEEZEBOX: 'media_player',
SERVICE_PANASONIC_VIERA: 'media_player',
SERVICE_ROKU: 'media_player',
}
@ -52,7 +56,7 @@ def listen(hass, service, callback):
def discovery_event_listener(event):
"""Listen for discovery events."""
if event.data[ATTR_SERVICE] in service:
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
@ -73,6 +77,32 @@ def discover(hass, service, discovered=None, component=None, hass_config=None):
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
def load_platform(hass, component, platform, info=None, hass_config=None):
"""Helper method for generic platform loading.
This method allows a platform to be loaded dynamically without it being
known at runtime (in the DISCOVERY_PLATFORMS list of the component).
Advantages of using this method:
- Any component & platforms combination can be dynamically added
- A component (i.e. light) does not have to import every component
that can dynamically add a platform (e.g. wemo, wink, insteon_hub)
- Custom user components can take advantage of discovery/loading
Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
fired to load the platform. The event will contain:
{ ATTR_SERVICE = LOAD_PLATFORM + '.' + <<component>>
ATTR_DISCOVERED = {LOAD_PLATFORM: <<platform>>} }
* dev note: This listener can be found in entity_component.py
"""
if info is None:
info = {LOAD_PLATFORM: platform}
else:
info[LOAD_PLATFORM] = platform
discover(hass, LOAD_PLATFORM + '.' + component, info, component,
hass_config)
def setup(hass, config):
"""Start a discovery service."""
logger = logging.getLogger(__name__)

View File

@ -22,7 +22,7 @@ HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/'
'92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4']
'4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5']
_LOGGER = logging.getLogger(__name__)

View File

@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
VERSION = "77c51c270b0241ce7ba0d1df2d254d6f"
VERSION = "0a226e905af198b2dabf1ce154844568"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 6a8e6a5a081415690bf89e87697d15b6ce9ebf8b
Subproject commit 4a667eb77e28a27dc766ca6f7bbd04e3866124d9

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

View File

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

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

View File

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

View File

@ -235,14 +235,16 @@ class HueLight(Light):
if ATTR_TRANSITION in kwargs:
command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10
if ATTR_BRIGHTNESS in kwargs:
command['bri'] = kwargs[ATTR_BRIGHTNESS]
if ATTR_XY_COLOR in kwargs:
command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs:
command['xy'] = color_util.color_RGB_to_xy(
xyb = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['xy'] = xyb[0], xyb[1]
command['bri'] = xyb[2]
if ATTR_BRIGHTNESS in kwargs:
command['bri'] = kwargs[ATTR_BRIGHTNESS]
if ATTR_COLOR_TEMP in kwargs:
command['ct'] = kwargs[ATTR_COLOR_TEMP]

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
example: '[255, 100, 100]'
color_name:
description: A human readable color name
example: 'red'
xy_color:
description: Color for the light in XY-format
example: '[0.52, 0.43]'

View File

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

View File

@ -97,7 +97,9 @@ class WinkLight(Light):
}
if rgb_color:
state_kwargs['color_xy'] = color_util.color_RGB_to_xy(*rgb_color)
xyb = color_util.color_RGB_to_xy(*rgb_color)
state_kwargs['color_xy'] = xyb[0], xyb[1]
state_kwargs['brightness'] = xyb[2]
if color_temp_mired:
state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired)

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

View File

@ -253,7 +253,7 @@ class CastDevice(MediaPlayerDevice):
"""Seek the media to a specific location."""
self.cast.media_controller.seek(position)
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL."""
self.cast.media_controller.play_media(media_id, media_type)

View File

@ -152,7 +152,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
"""Flag of media commands that are supported."""
return YOUTUBE_PLAYER_SUPPORT
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
self.youtube_id = media_id
self.update_ha_state()

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()
self.update_state(response)
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the media player."""
if media_type == MEDIA_TYPE_PLAYLIST:
response = self.client.play_playlist(media_id)

View File

@ -278,6 +278,6 @@ class KodiDevice(MediaPlayerDevice):
self.update_ha_state()
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the media player."""
self._server.Player.Open({media_type: media_id}, {})

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:
self.status = self.client.status()
self.currentsong = self.client.currentsong()
except mpd.ConnectionError:
except (mpd.ConnectionError, BrokenPipeError, ValueError):
# Cleanly disconnect in case connection is not in valid state
try:
self.client.disconnect()
except mpd.ConnectionError:
pass
self.client.connect(self.server, self.port)
if self.password is not None:
@ -206,7 +212,7 @@ class MpdDevice(MediaPlayerDevice):
"""Service to send the MPD the command for previous track."""
self.client.previous()
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Send the media player the command for playing a playlist."""
_LOGGER.info(str.format("Playing playlist: {0}", media_id))
if media_type == MEDIA_TYPE_PLAYLIST:

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME
REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/'
'python3.zip#onkyo-eiscp==0.9.2']
@ -17,29 +17,59 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
KNOWN_HOSTS = []
DEFAULT_SOURCES = {"tv": "TV", "bd": "Bluray", "game": "Game", "aux1": "Aux1",
"video1": "Video 1", "video2": "Video 2",
"video3": "Video 3", "video4": "Video 4",
"video5": "Video 5", "video6": "Video 6",
"video7": "Video 7"}
CONFIG_SOURCE_LIST = "sources"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Onkyo platform."""
import eiscp
from eiscp import eISCP
add_devices(OnkyoDevice(receiver)
for receiver in eISCP.discover())
hosts = []
if CONF_HOST in config and config[CONF_HOST] not in KNOWN_HOSTS:
try:
hosts.append(OnkyoDevice(eiscp.eISCP(config[CONF_HOST]),
config.get(CONFIG_SOURCE_LIST,
DEFAULT_SOURCES),
name=config[CONF_NAME]))
KNOWN_HOSTS.append(config[CONF_HOST])
except OSError:
_LOGGER.error('Unable to connect to receiver at %s.',
config[CONF_HOST])
else:
for receiver in eISCP.discover():
if receiver.host not in KNOWN_HOSTS:
hosts.append(OnkyoDevice(receiver,
config.get(CONFIG_SOURCE_LIST,
DEFAULT_SOURCES)))
KNOWN_HOSTS.append(receiver.host)
add_devices(hosts)
# pylint: disable=too-many-instance-attributes
class OnkyoDevice(MediaPlayerDevice):
"""Representation of a Onkyo device."""
# pylint: disable=too-many-public-methods, abstract-method
def __init__(self, receiver):
def __init__(self, receiver, sources, name=None):
"""Initialize the Onkyo Receiver."""
self._receiver = receiver
self._muted = False
self._volume = 0
self._pwstate = STATE_OFF
self.update()
self._name = '{}_{}'.format(
self._name = name or '{}_{}'.format(
receiver.info['model_name'], receiver.info['identifier'])
self._current_source = None
self._source_list = list(sources.values())
self._source_mapping = sources
self._reverse_mapping = {value: key for key, value in sources.items()}
self.update()
def update(self):
"""Get the latest details from the device."""
@ -52,8 +82,13 @@ class OnkyoDevice(MediaPlayerDevice):
volume_raw = self._receiver.command('volume query')
mute_raw = self._receiver.command('audio-muting query')
current_source_raw = self._receiver.command('input-selector query')
self._current_source = '_'.join('_'.join(
[i for i in current_source_raw[1]]))
for source in current_source_raw[1]:
if source in self._source_mapping:
self._current_source = self._source_mapping[source]
break
else:
self._current_source = '_'.join(
[i for i in current_source_raw[1]])
self._muted = bool(mute_raw[1] == 'on')
self._volume = int(volume_raw[1], 16)/80.0
@ -87,6 +122,11 @@ class OnkyoDevice(MediaPlayerDevice):
""""Return the current input source of the device."""
return self._current_source
@property
def source_list(self):
"""List of available input sources."""
return self._source_list
def turn_off(self):
"""Turn off media player."""
self._receiver.command('system-power standby')
@ -108,4 +148,6 @@ class OnkyoDevice(MediaPlayerDevice):
def select_source(self, source):
"""Set the input source."""
if source in self._source_list:
source = self._reverse_mapping[source]
self._receiver.command('input-selector {}'.format(source))

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:
description: Name of the source to switch to. Platform dependent.
example: 'video1'
sonos_group_players:
description: Send Sonos media player the command for grouping all players into one (party mode).
fields:
entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
example: 'media_player.living_room_sonos'

View File

@ -7,14 +7,15 @@ https://home-assistant.io/components/media_player.sonos/
import datetime
import logging
import socket
from os import path
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['SoCo==0.11.1']
@ -32,6 +33,8 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
SUPPORT_SEEK
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
@ -63,9 +66,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.warning('No Sonos speakers found.')
return False
add_devices(SonosDevice(hass, p) for p in players)
devices = [SonosDevice(hass, p) for p in players]
add_devices(devices)
_LOGGER.info('Added %s Sonos speakers', len(players))
def group_players_service(service):
"""Group media players, use player as coordinator."""
entity_id = service.data.get('entity_id')
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
device.group_players()
device.update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS,
group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS))
return True
@ -74,16 +99,26 @@ def only_if_coordinator(func):
If used as decorator, avoid calling the decorated method if player is not
a coordinator. If not, a grouped speaker (not in coordinator role) will
throw soco.exceptions.SoCoSlaveException
throw soco.exceptions.SoCoSlaveException.
Also, partially catch exceptions like:
soco.exceptions.SoCoUPnPException: UPnP Error 701 received:
Transition not available from <player ip address>
"""
def wrapper(*args, **kwargs):
"""Decorator wrapper."""
if args[0].is_coordinator:
return func(*args, **kwargs)
from soco.exceptions import SoCoUPnPException
try:
func(*args, **kwargs)
except SoCoUPnPException:
_LOGGER.error('command "%s" for Sonos device "%s" '
'not available in this mode',
func.__name__, args[0].name)
else:
_LOGGER.debug('Ignore command "%s" for Sonos device "%s" '
'(not coordinator)',
func.__name__, args[0].name)
_LOGGER.debug('Ignore command "%s" for Sonos device "%s" (%s)',
func.__name__, args[0].name, 'not coordinator')
return wrapper
@ -104,7 +139,7 @@ class SonosDevice(MediaPlayerDevice):
@property
def should_poll(self):
"""No polling needed."""
"""Polling needed."""
return True
def update_sonos(self, now):
@ -258,9 +293,27 @@ class SonosDevice(MediaPlayerDevice):
self._player.play()
@only_if_coordinator
def play_media(self, media_type, media_id):
"""Send the play_media command to the media player."""
self._player.play_uri(media_id)
def play_media(self, media_type, media_id, **kwargs):
"""
Send the play_media command to the media player.
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
"""
if kwargs.get(ATTR_MEDIA_ENQUEUE):
from soco.exceptions import SoCoUPnPException
try:
self._player.add_uri_to_queue(media_id)
except SoCoUPnPException:
_LOGGER.error('Error parsing media uri "%s", '
"please check it's a valid media resource "
'supported by Sonos', media_id)
else:
self._player.play_uri(media_id)
@only_if_coordinator
def group_players(self):
"""Group all players under this coordinator."""
self._player.partymode()
@property
def available(self):

View File

@ -402,7 +402,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
data = {ATTR_MEDIA_SEEK_POSITION: position}
self._call_service(SERVICE_MEDIA_SEEK, data)
def play_media(self, media_type, media_id):
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
data = {ATTR_MEDIA_CONTENT_TYPE: media_type,
ATTR_MEDIA_CONTENT_ID: media_id}

View File

@ -388,6 +388,8 @@ class MQTT(object):
def _mqtt_on_message(self, _mqttc, _userdata, msg):
"""Message received callback."""
_LOGGER.debug("received message on %s: %s",
msg.topic, msg.payload.decode('utf-8'))
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
ATTR_TOPIC: msg.topic,
ATTR_QOS: msg.qos,

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
https://home-assistant.io/components/thermostat.nest/
@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
REQUIREMENTS = ['python-nest==2.6.0']
REQUIREMENTS = ['python-nest==2.9.2']
DOMAIN = 'nest'
NEST = None
@ -36,6 +36,16 @@ def devices():
_LOGGER.error("Connection error logging into the nest web service.")
def protect_devices():
"""Generator returning list of protect devices."""
try:
for structure in NEST.structures:
for device in structure.protectdevices:
yield(structure, device)
except socket.error:
_LOGGER.error("Connection error logging into the nest web service.")
# pylint: disable=unused-argument
def setup(hass, config):
"""Setup the Nest thermostat component."""

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
def __init__(self, app_name, app_icon, hostname, password, port):
"""Initialize the service."""
from gntp import notifier
self.gntp = notifier.GrowlNotifier(
import gntp.notifier
import gntp.errors
self.gntp = gntp.notifier.GrowlNotifier(
applicationName=app_name,
notifications=["Notification"],
applicationIcon=app_icon,
@ -50,7 +51,11 @@ class GNTPNotificationService(BaseNotificationService):
password=password,
port=port
)
self.gntp.register()
try:
self.gntp.register()
except gntp.errors.NetworkError:
_LOGGER.error('Unable to register with the GNTP host.')
return
def send_message(self, message="", **kwargs):
"""Send a message to a user."""

View File

@ -2,7 +2,7 @@
Google Voice SMS platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.free_mobile/
https://home-assistant.io/components/notify.google_voice/
"""
import logging

View File

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

View File

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

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 sqlite3
import threading
from datetime import date, datetime
from datetime import date, datetime, timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.const import (
@ -21,6 +22,7 @@ from homeassistant.const import (
EVENT_TIME_CHANGED, MATCH_ALL)
from homeassistant.core import Event, EventOrigin, State
from homeassistant.remote import JSONEncoder
from homeassistant.helpers.event import track_point_in_utc_time
DOMAIN = "recorder"
@ -30,6 +32,15 @@ RETURN_ROWCOUNT = "rowcount"
RETURN_LASTROWID = "lastrowid"
RETURN_ONE_ROW = "one_row"
CONF_PURGE_DAYS = "purge_days"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
vol.Range(min=1)),
})
}, extra=vol.ALLOW_EXTRA)
_INSTANCE = None
_LOGGER = logging.getLogger(__name__)
@ -102,14 +113,14 @@ def setup(hass, config):
"""Setup the recorder."""
# pylint: disable=global-statement
global _INSTANCE
_INSTANCE = Recorder(hass)
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
_INSTANCE = Recorder(hass, purge_days=purge_days)
return True
class RecorderRun(object):
"""Representation of arecorder run."""
"""Representation of a recorder run."""
def __init__(self, row=None):
"""Initialize the recorder run."""
@ -169,11 +180,12 @@ class Recorder(threading.Thread):
"""A threaded recorder class."""
# pylint: disable=too-many-instance-attributes
def __init__(self, hass):
def __init__(self, hass, purge_days):
"""Initialize the recorder."""
threading.Thread.__init__(self)
self.hass = hass
self.purge_days = purge_days
self.conn = None
self.queue = queue.Queue()
self.quit_object = object()
@ -194,6 +206,10 @@ class Recorder(threading.Thread):
"""Start processing events to save."""
self._setup_connection()
self._setup_run()
if self.purge_days is not None:
track_point_in_utc_time(self.hass,
lambda now: self._purge_old_data(),
dt_util.utcnow() + timedelta(minutes=5))
while True:
event = self.queue.get()
@ -475,6 +491,32 @@ class Recorder(threading.Thread):
"UPDATE recorder_runs SET end=? WHERE start=?",
(dt_util.utcnow(), self.recording_start))
def _purge_old_data(self):
"""Purge events and states older than purge_days ago."""
if not self.purge_days or self.purge_days < 1:
_LOGGER.debug("purge_days set to %s, will not purge any old data.",
self.purge_days)
return
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
_LOGGER.info("Purging events created before %s", purge_before)
deleted_rows = self.query(
sql_query="DELETE FROM events WHERE created < ?;",
data=(int(purge_before.timestamp()),),
return_value=RETURN_ROWCOUNT)
_LOGGER.debug("Deleted %s events", deleted_rows)
_LOGGER.info("Purging states created before %s", purge_before)
deleted_rows = self.query(
sql_query="DELETE FROM states WHERE created < ?;",
data=(int(purge_before.timestamp()),),
return_value=RETURN_ROWCOUNT)
_LOGGER.debug("Deleted %s states", deleted_rows)
# Execute sqlite vacuum command to free up space on disk
self.query("VACUUM;")
def _adapt_datetime(datetimestamp):
"""Turn a datetime into an integer for in the DB."""

View File

@ -2,7 +2,7 @@
Support for Powerview scenes from a Powerview hub.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/scene/
https://home-assistant.io/components/scene.hunterdouglas_powerview/
"""
import logging

View File

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

View File

@ -10,8 +10,10 @@ import logging
import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@ -23,47 +25,102 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
CONF_ORIGIN = 'origin'
CONF_DESTINATION = 'destination'
CONF_TRAVEL_MODE = 'travel_mode'
CONF_OPTIONS = 'options'
CONF_MODE = 'mode'
CONF_NAME = 'name'
ALL_LANGUAGES = ['ar', 'bg', 'bn', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es',
'eu', 'fa', 'fi', 'fr', 'gl', 'gu', 'hi', 'hr', 'hu', 'id',
'it', 'iw', 'ja', 'kn', 'ko', 'lt', 'lv', 'ml', 'mr', 'nl',
'no', 'pl', 'pt', 'pt-BR', 'pt-PT', 'ro', 'ru', 'sk', 'sl',
'sr', 'sv', 'ta', 'te', 'th', 'tl', 'tr', 'uk', 'vi',
'zh-CN', 'zh-TW']
TRANSIT_PREFS = ['less_walking', 'fewer_transfers']
PLATFORM_SCHEMA = vol.Schema({
vol.Required('platform'): 'google_travel_time',
vol.Optional(CONF_NAME): vol.Coerce(str),
vol.Required(CONF_API_KEY): vol.Coerce(str),
vol.Required(CONF_ORIGIN): vol.Coerce(str),
vol.Required(CONF_DESTINATION): vol.Coerce(str),
vol.Optional(CONF_TRAVEL_MODE, default='driving'):
vol.In(["driving", "walking", "bicycling", "transit"])
vol.Optional(CONF_TRAVEL_MODE):
vol.In(["driving", "walking", "bicycling", "transit"]),
vol.Optional(CONF_OPTIONS, default=dict()): vol.All(
dict, vol.Schema({
vol.Optional(CONF_MODE, default='driving'):
vol.In(["driving", "walking", "bicycling", "transit"]),
vol.Optional('language'): vol.In(ALL_LANGUAGES),
vol.Optional('avoid'): vol.In(['tolls', 'highways',
'ferries', 'indoor']),
vol.Optional('units'): vol.In(['metric', 'imperial']),
vol.Exclusive('arrival_time', 'time'): cv.string,
vol.Exclusive('departure_time', 'time'): cv.string,
vol.Optional('traffic_model'): vol.In(['best_guess',
'pessimistic',
'optimistic']),
vol.Optional('transit_mode'): vol.In(['bus', 'subway', 'train',
'tram', 'rail']),
vol.Optional('transit_routing_preference'): vol.In(TRANSIT_PREFS)
}))
})
def convert_time_to_utc(timestr):
"""Take a string like 08:00:00 and convert it to a unix timestamp."""
combined = datetime.combine(dt_util.start_of_local_day(),
dt_util.parse_time(timestr))
if combined < datetime.now():
combined = combined + timedelta(days=1)
return dt_util.as_timestamp(combined)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the travel time platform."""
# pylint: disable=too-many-locals
options = config.get(CONF_OPTIONS)
is_metric = (hass.config.temperature_unit == TEMP_CELSIUS)
if options.get('units') is None:
if hass.config.temperature_unit is TEMP_CELSIUS:
options['units'] = 'metric'
elif hass.config.temperature_unit is TEMP_FAHRENHEIT:
options['units'] = 'imperial'
travel_mode = config.get(CONF_TRAVEL_MODE)
mode = options.get(CONF_MODE)
if travel_mode is not None:
wstr = ("Google Travel Time: travel_mode is deprecated, please add "
"mode to the options dictionary instead!")
_LOGGER.warning(wstr)
if mode is None:
options[CONF_MODE] = travel_mode
titled_mode = options.get(CONF_MODE).title()
formatted_name = "Google Travel Time - {}".format(titled_mode)
name = config.get(CONF_NAME, formatted_name)
api_key = config.get(CONF_API_KEY)
origin = config.get(CONF_ORIGIN)
destination = config.get(CONF_DESTINATION)
travel_mode = config.get(CONF_TRAVEL_MODE)
sensor = GoogleTravelTimeSensor(api_key, origin, destination,
travel_mode, is_metric)
sensor = GoogleTravelTimeSensor(name, api_key, origin, destination,
options)
if sensor.valid_api_connection:
add_devices_callback([sensor])
# pylint: disable=too-many-instance-attributes
class GoogleTravelTimeSensor(Entity):
"""Representation of a tavel time sensor."""
# pylint: disable=too-many-arguments
def __init__(self, api_key, origin, destination, travel_mode, is_metric):
def __init__(self, name, api_key, origin, destination, options):
"""Initialize the sensor."""
if is_metric:
self._unit = 'metric'
else:
self._unit = 'imperial'
self._name = name
self._options = options
self._origin = origin
self._destination = destination
self._travel_mode = travel_mode
self._matrix = None
self.valid_api_connection = True
@ -79,17 +136,22 @@ class GoogleTravelTimeSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
return self._matrix['rows'][0]['elements'][0]['duration']['value']/60.0
try:
res = self._matrix['rows'][0]['elements'][0]['duration']['value']
return round(res/60)
except KeyError:
return None
@property
def name(self):
"""Get the name of the sensor."""
return "Google Travel time"
return self._name
@property
def device_state_attributes(self):
"""Return the state attributes."""
res = self._matrix.copy()
res.update(self._options)
del res['rows']
_data = self._matrix['rows'][0]['elements'][0]
if 'duration_in_traffic' in _data:
@ -108,10 +170,15 @@ class GoogleTravelTimeSensor(Entity):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from Google."""
now = datetime.now()
options_copy = self._options.copy()
dtime = options_copy.get('departure_time')
atime = options_copy.get('arrival_time')
if dtime is not None and ':' in dtime:
options_copy['departure_time'] = convert_time_to_utc(dtime)
if atime is not None and ':' in atime:
options_copy['arrival_time'] = convert_time_to_utc(atime)
self._matrix = self._client.distance_matrix(self._origin,
self._destination,
mode=self._travel_mode,
units=self._unit,
departure_time=now,
traffic_model="optimistic")
**options_copy)

View File

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

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"
REQUIREMENTS = ['pyloopenergy==0.0.10']
REQUIREMENTS = ['pyloopenergy==0.0.12']
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
https://home-assistant.io/components/sensor.nest/
"""
from itertools import chain
import voluptuous as vol
import homeassistant.components.nest as nest
@ -29,9 +31,13 @@ WEATHER_VARS = {'weather_humidity': 'humidity',
SENSOR_UNITS = {'humidity': '%', 'battery_level': 'V',
'kph': 'kph', 'temperature': '°C'}
PROTECT_VARS = ['co_status',
'smoke_status',
'battery_level']
SENSOR_TEMP_TYPES = ['temperature', 'target']
_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + \
_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS + \
list(WEATHER_VARS.keys())
PLATFORM_SCHEMA = vol.Schema({
@ -44,20 +50,34 @@ PLATFORM_SCHEMA = vol.Schema({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Nest Sensor."""
for structure, device in nest.devices():
for structure, device in chain(nest.devices(), nest.protect_devices()):
sensors = [NestBasicSensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in SENSOR_TYPES]
if variable in SENSOR_TYPES and is_thermostat(device)]
sensors += [NestTempSensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in SENSOR_TEMP_TYPES]
if variable in SENSOR_TEMP_TYPES and is_thermostat(device)]
sensors += [NestWeatherSensor(structure, device,
WEATHER_VARS[variable])
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in WEATHER_VARS]
if variable in WEATHER_VARS and is_thermostat(device)]
sensors += [NestProtectSensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in PROTECT_VARS and is_protect(device)]
add_devices(sensors)
def is_thermostat(device):
"""Target devices that are Nest Thermostats."""
return bool(device.__class__.__name__ == 'Device')
def is_protect(device):
"""Target devices that are Nest Protect Smoke Alarms."""
return bool(device.__class__.__name__ == 'ProtectDevice')
class NestSensor(Entity):
"""Representation of a Nest sensor."""
@ -130,3 +150,28 @@ class NestWeatherSensor(NestSensor):
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return SENSOR_UNITS.get(self.variable, None)
class NestProtectSensor(NestSensor):
"""Return the state of nest protect."""
@property
def state(self):
"""Return the state of the sensor."""
state = getattr(self.device, self.variable)
if self.variable == 'battery_level':
return getattr(self.device, self.variable)
else:
if state == 0:
return 'Ok'
if state == 1 or state == 2:
return 'Warning'
if state == 3:
return 'Emergency'
return 'Unknown'
@property
def name(self):
"""Return the name of the nest, if any."""
return "{} {}".format(self.device.where.capitalize(), self.variable)

View File

@ -7,11 +7,12 @@ https://home-assistant.io/components/sensor.speedtest/
import logging
import re
import sys
from datetime import timedelta
from subprocess import check_output
import homeassistant.util.dt as dt_util
from homeassistant.components.sensor import DOMAIN
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.helpers.event import track_time_change
REQUIREMENTS = ['speedtest-cli==0.3.4']
_LOGGER = logging.getLogger(__name__)
@ -21,6 +22,7 @@ _SPEEDTEST_REGEX = re.compile(r'Ping:\s(\d+\.\d+)\sms[\r\n]+'
r'Upload:\s(\d+\.\d+)\sMbit/s[\r\n]+')
CONF_MONITORED_CONDITIONS = 'monitored_conditions'
CONF_SECOND = 'second'
CONF_MINUTE = 'minute'
CONF_HOUR = 'hour'
CONF_DAY = 'day'
@ -30,13 +32,10 @@ SENSOR_TYPES = {
'upload': ['Upload', 'Mbit/s'],
}
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Speedtest sensor."""
data = SpeedtestData()
data = SpeedtestData(hass, config)
dev = []
for sensor in config[CONF_MONITORED_CONDITIONS]:
if sensor not in SENSOR_TYPES:
@ -46,6 +45,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(dev)
def update(call=None):
"""Update service for manual updates."""
data.update(dt_util.now())
for sensor in dev:
sensor.update()
hass.services.register(DOMAIN, 'update_speedtest', update)
# pylint: disable=too-few-public-methods
class SpeedtestSensor(Entity):
@ -76,7 +83,6 @@ class SpeedtestSensor(Entity):
def update(self):
"""Get the latest data and update the states."""
self.speedtest_client.update()
data = self.speedtest_client.data
if data is None:
return
@ -92,12 +98,16 @@ class SpeedtestSensor(Entity):
class SpeedtestData(object):
"""Get the latest data from speedtest.net."""
def __init__(self):
def __init__(self, hass, config):
"""Initialize the data object."""
self.data = None
track_time_change(hass, self.update,
second=config.get(CONF_SECOND, 0),
minute=config.get(CONF_MINUTE, 0),
hour=config.get(CONF_HOUR, None),
day=config.get(CONF_DAY, None))
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
def update(self, now):
"""Get the latest data from speedtest.net."""
import speedtest_cli

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.helpers.entity import Entity
REQUIREMENTS = ['psutil==4.1.0']
REQUIREMENTS = ['psutil==4.2.0']
SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'],
'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'],
@ -24,9 +24,9 @@ SENSOR_TYPES = {
'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'],
'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'],
'network_out': ['Sent', 'MiB', 'mdi:server-network'],
'network_in': ['Recieved', 'MiB', 'mdi:server-network'],
'network_in': ['Received', 'MiB', 'mdi:server-network'],
'packets_out': ['Packets sent', '', 'mdi:server-network'],
'packets_in': ['Packets recieved', '', 'mdi:server-network'],
'packets_in': ['Packets received', '', 'mdi:server-network'],
'ipv4_address': ['IPv4 address', '', 'mdi:server-network'],
'ipv6_address': ['IPv6 address', '', 'mdi:server-network'],
'last_boot': ['Last Boot', '', 'mdi:clock'],

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__)
# pylint: disable=unused-argument
# pylint: disable=unused-argument, import-error
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Find and return switches controlled by a generic RF device via GPIO."""
import rpi_rf

View File

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

View File

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

View File

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

View File

@ -55,12 +55,23 @@ class EntityComponent(object):
self._setup_platform(p_type, p_config)
if self.discovery_platforms:
# Discovery listener for all items in discovery_platforms array
# passed from a component's setup method (e.g. light/__init__.py)
discovery.listen(
self.hass, self.discovery_platforms.keys(),
lambda service, info:
self._setup_platform(self.discovery_platforms[service], {},
info))
# Generic discovery listener for loading platform dynamically
# Refer to: homeassistant.components.discovery.load_platform()
def load_platform_callback(service, info):
"""Callback to load a platform."""
platform = info.pop(discovery.LOAD_PLATFORM)
self._setup_platform(platform, {}, info if info else None)
discovery.listen(self.hass, discovery.LOAD_PLATFORM + '.' +
self.domain, load_platform_callback)
def extract_from_service(self, service):
"""Extract all known entities from a service call.

View File

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

View File

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

View File

@ -1,48 +1,47 @@
"""Color util methods."""
import math
# pylint: disable=unused-import
from webcolors import html5_parse_legacy_color as color_name_to_rgb # noqa
HASS_COLOR_MAX = 500 # mireds (inverted)
HASS_COLOR_MIN = 154
# Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py
# Taken from:
# http://www.developers.meethue.com/documentation/color-conversions-rgb-xy
# License: Code is given as is. Use at your own risk and discretion.
# pylint: disable=invalid-name
def color_RGB_to_xy(R, G, B):
"""Convert from RGB color to XY color."""
if R + G + B == 0:
return 0, 0
return 0, 0, 0
var_R = (R / 255.)
var_G = (G / 255.)
var_B = (B / 255.)
R = R / 255
B = B / 255
G = G / 255
if var_R > 0.04045:
var_R = ((var_R + 0.055) / 1.055) ** 2.4
else:
var_R /= 12.92
# Gamma correction
R = pow((R + 0.055) / (1.0 + 0.055),
2.4) if (R > 0.04045) else (R / 12.92)
G = pow((G + 0.055) / (1.0 + 0.055),
2.4) if (G > 0.04045) else (G / 12.92)
B = pow((B + 0.055) / (1.0 + 0.055),
2.4) if (B > 0.04045) else (B / 12.92)
if var_G > 0.04045:
var_G = ((var_G + 0.055) / 1.055) ** 2.4
else:
var_G /= 12.92
# Wide RGB D65 conversion formula
X = R * 0.664511 + G * 0.154324 + B * 0.162028
Y = R * 0.313881 + G * 0.668433 + B * 0.047685
Z = R * 0.000088 + G * 0.072310 + B * 0.986039
if var_B > 0.04045:
var_B = ((var_B + 0.055) / 1.055) ** 2.4
else:
var_B /= 12.92
# Convert XYZ to xy
x = X / (X + Y + Z)
y = Y / (X + Y + Z)
var_R *= 100
var_G *= 100
var_B *= 100
# Brightness
Y = 1 if Y > 1 else Y
brightness = round(Y * 255)
# Observer. = 2 deg, Illuminant = D65
X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
# Convert XYZ to xy, see CIE 1931 color space on wikipedia
return X / (X + Y + Z), Y / (X + Y + Z)
return round(x, 3), round(y, 3), brightness
# taken from

View File

@ -152,3 +152,52 @@ def parse_time(time_str):
except ValueError:
# ValueError if value cannot be converted to an int or not in range
return None
# Found in this gist: https://gist.github.com/zhangsen/1199964
def get_age(date):
# pylint: disable=too-many-return-statements
"""
Take a datetime and return its "age" as a string.
The age can be in second, minute, hour, day, month or year. Only the
biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will
be returned.
Make sure date is not in the future, or else it won't work.
"""
def formatn(number, unit):
"""Add "unit" if it's plural."""
if number == 1:
return "1 %s" % unit
elif number > 1:
return "%d %ss" % (number, unit)
def q_n_r(first, second):
"""Return quotient and remaining."""
return first // second, first % second
delta = now() - date
day = delta.days
second = delta.seconds
year, day = q_n_r(day, 365)
if year > 0:
return formatn(year, 'year')
month, day = q_n_r(day, 30)
if month > 0:
return formatn(month, 'month')
if day > 0:
return formatn(day, 'day')
hour, second = q_n_r(second, 3600)
if hour > 0:
return formatn(hour, 'hour')
minute, second = q_n_r(second, 60)
if minute > 0:
return formatn(minute, 'minute')
if second > 0:
return formatn(second, 'second')
return "0 second"

View File

@ -3,6 +3,7 @@ import logging
import os
from collections import OrderedDict
import glob
import yaml
from homeassistant.exceptions import HomeAssistantError
@ -44,6 +45,44 @@ def _include_yaml(loader, node):
return load_yaml(fname)
def _include_dir_named_yaml(loader, node):
"""Load multiple files from dir as a dict."""
mapping = OrderedDict()
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
for fname in glob.glob(files):
filename = os.path.splitext(os.path.basename(fname))[0]
mapping[filename] = load_yaml(fname)
return mapping
def _include_dir_merge_named_yaml(loader, node):
"""Load multiple files from dir as a merged dict."""
mapping = OrderedDict()
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
for fname in glob.glob(files):
loaded_yaml = load_yaml(fname)
if isinstance(loaded_yaml, dict):
mapping.update(loaded_yaml)
return mapping
def _include_dir_list_yaml(loader, node):
"""Load multiple files from dir as a list."""
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
return [load_yaml(f) for f in glob.glob(files)]
def _include_dir_merge_list_yaml(loader, node):
"""Load multiple files from dir as a merged list."""
files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml')
merged_list = []
for fname in glob.glob(files):
loaded_yaml = load_yaml(fname)
if isinstance(loaded_yaml, list):
merged_list.extend(loaded_yaml)
return merged_list
def _ordered_dict(loader, node):
"""Load YAML mappings into an ordered dict to preserve key order."""
loader.flatten_mapping(node)
@ -84,3 +123,9 @@ yaml.SafeLoader.add_constructor('!include', _include_yaml)
yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
_ordered_dict)
yaml.SafeLoader.add_constructor('!env_var', _env_var_yaml)
yaml.SafeLoader.add_constructor('!include_dir_list', _include_dir_list_yaml)
yaml.SafeLoader.add_constructor('!include_dir_merge_list',
_include_dir_merge_list_yaml)
yaml.SafeLoader.add_constructor('!include_dir_named', _include_dir_named_yaml)
yaml.SafeLoader.add_constructor('!include_dir_merge_named',
_include_dir_merge_named_yaml)

View File

@ -6,6 +6,7 @@ pip>=7.0.0
vincenty==0.1.4
jinja2>=2.8
voluptuous==0.8.9
webcolors==1.5
# homeassistant.components.isy994
PyISY==1.0.5
@ -37,6 +38,11 @@ blockchain==1.3.1
# homeassistant.components.thermostat.eq3btsmart
# bluepy_devices>=0.2.0
# homeassistant.components.notify.aws_lambda
# homeassistant.components.notify.aws_sns
# homeassistant.components.notify.aws_sqs
boto3==1.3.1
# homeassistant.components.notify.xmpp
dnspython3==1.12.0
@ -97,6 +103,9 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl
# homeassistant.components.alarm_control_panel.alarmdotcom
https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1
# homeassistant.components.media_player.roku
https://github.com/bah2830/python-roku/archive/3.1.1.zip#python-roku==3.1.1
# homeassistant.components.modbus
https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0
@ -109,8 +118,11 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9.
# homeassistant.components.sensor.sabnzbd
https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
# homeassistant.components.qwikswitch
https://github.com/kellerza/pyqwikswitch/archive/v0.3.zip#pyqwikswitch==0.3
# homeassistant.components.ecobee
https://github.com/nkgilley/python-ecobee-api/archive/92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4
https://github.com/nkgilley/python-ecobee-api/archive/4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5
# homeassistant.components.switch.edimax
https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1
@ -119,7 +131,7 @@ https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f
https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3
# homeassistant.components.sensor.gtfs
https://github.com/robbiet480/pygtfs/archive/432414b720c580fb2667a0a48f539118a2d95969.zip#pygtfs==0.1.2
https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3
# homeassistant.components.scene.hunterdouglas_powerview
https://github.com/sander76/powerviewApi/archive/master.zip#powerviewApi==0.2
@ -130,6 +142,9 @@ https://github.com/theolind/pymysensors/archive/cc5d0b325e13c2b623fa934f69eea7cd
# homeassistant.components.notify.googlevoice
https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#pygooglevoice-sms==0.0.1
# homeassistant.components.media_player.lg_netcast
https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0
# homeassistant.components.influxdb
influxdb==2.12.0
@ -153,7 +168,7 @@ messagebird==1.2.0
mficlient==0.3.0
# homeassistant.components.discovery
netdisco==0.6.6
netdisco==0.6.7
# homeassistant.components.sensor.neurio_energy
neurio==0.2.10
@ -168,6 +183,7 @@ paho-mqtt==1.1
panasonic_viera==0.2
# homeassistant.components.device_tracker.aruba
# homeassistant.components.device_tracker.asuswrt
pexpect==4.0.1
# homeassistant.components.light.hue
@ -180,7 +196,7 @@ plexapi==1.1.0
proliphix==0.1.0
# homeassistant.components.sensor.systemmonitor
psutil==4.1.0
psutil==4.2.0
# homeassistant.components.notify.pushbullet
pushbullet.py==0.10.0
@ -215,8 +231,11 @@ pyfttt==0.3
# homeassistant.components.device_tracker.icloud
pyicloud==0.8.3
# homeassistant.components.sensor.lastfm
pylast==1.6.0
# homeassistant.components.sensor.loopenergy
pyloopenergy==0.0.10
pyloopenergy==0.0.12
# homeassistant.components.device_tracker.netgear
pynetgear==0.3.3
@ -241,7 +260,7 @@ python-forecastio==1.3.4
python-mpd2==0.5.5
# homeassistant.components.nest
python-nest==2.6.0
python-nest==2.9.2
# homeassistant.components.device_tracker.nmap_tracker
python-nmap==0.6.0
@ -253,7 +272,7 @@ python-pushover==0.2
python-statsd==1.7.2
# homeassistant.components.notify.telegram
python-telegram-bot==4.0.1
python-telegram-bot==4.1.1
# homeassistant.components.sensor.twitch
python-twitch==1.2.0
@ -280,7 +299,7 @@ pywemo==0.4.2
radiotherm==1.2
# homeassistant.components.switch.rpi_rf
rpi-rf==0.9.5
# rpi-rf==0.9.5
# homeassistant.components.media_player.yamaha
rxv==0.1.11
@ -326,6 +345,9 @@ tellive-py==0.5.2
# homeassistant.components.switch.transmission
transmissionrpc==0.11
# homeassistant.components.notify.twilio_sms
twilio==5.4.0
# homeassistant.components.sensor.uber
uber_rides==0.2.1
@ -344,6 +366,9 @@ vsure==0.8.1
# homeassistant.components.switch.wake_on_lan
wakeonlan==0.2.2
# homeassistant.components.media_player.gpmdp
websocket-client==0.35.0
# homeassistant.components.zigbee
xbee-helper==0.0.7

View File

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

View File

@ -15,7 +15,7 @@ if [ -d python-openzwave ]; then
git pull --recurse-submodules=yes
git submodule update --init --recursive
else
git clone --recursive https://github.com/OpenZWave/python-openzwave.git
git clone --recursive --depth 1 https://github.com/OpenZWave/python-openzwave.git
cd python-openzwave
fi

View File

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

View File

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

View File

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

View File

@ -55,6 +55,21 @@ LOCATION_MESSAGE_INACCURATE = {
'tst': 1,
'vel': 0}
LOCATION_MESSAGE_ZERO_ACCURACY = {
'batt': 92,
'cog': 248,
'tid': 'user',
'lon': 2.0,
't': 'u',
'alt': 27,
'acc': 0,
'p': 101.3977584838867,
'vac': 4,
'lat': 6.0,
'_type': 'location',
'tst': 1,
'vel': 0}
REGION_ENTER_MESSAGE = {
'lon': 1.0,
'event': 'enter',
@ -204,6 +219,14 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase):
self.assert_location_latitude(2.0)
self.assert_location_longitude(1.0)
def test_location_zero_accuracy_gps(self):
"""Ignore the location for zero accuracy GPS information."""
self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE)
self.send_message(LOCATION_TOPIC, LOCATION_MESSAGE_ZERO_ACCURACY)
self.assert_location_latitude(2.0)
self.assert_location_longitude(1.0)
def test_event_entry_exit(self):
"""Test the entry event."""
self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)
@ -230,6 +253,20 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase):
# Left clean zone state
self.assertFalse(owntracks.REGIONS_ENTERED[USER])
def test_event_with_spaces(self):
"""Test the entry event."""
message = REGION_ENTER_MESSAGE.copy()
message['desc'] = "inner 2"
self.send_message(EVENT_TOPIC, message)
self.assert_location_state('inner_2')
message = REGION_LEAVE_MESSAGE.copy()
message['desc'] = "inner 2"
self.send_message(EVENT_TOPIC, message)
# Left clean zone state
self.assertFalse(owntracks.REGIONS_ENTERED[USER])
def test_event_entry_exit_inaccurate(self):
"""Test the event for inaccurate exit."""
self.send_message(EVENT_TOPIC, REGION_ENTER_MESSAGE)

View File

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

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."""
# pylint: disable=too-many-public-methods,protected-access
import unittest
import time
import json
from unittest.mock import patch
from homeassistant.const import MATCH_ALL
@ -10,7 +12,7 @@ from tests.common import get_test_home_assistant
class TestRecorder(unittest.TestCase):
"""Test the chromecast module."""
"""Test the recorder module."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
@ -25,6 +27,52 @@ class TestRecorder(unittest.TestCase):
self.hass.stop()
recorder._INSTANCE.block_till_done()
def _add_test_states(self):
"""Add multiple states to the db for testing."""
now = int(time.time())
five_days_ago = now - (60*60*24*5)
attributes = {'test_attr': 5, 'test_attr_10': 'nice'}
self.hass.pool.block_till_done()
recorder._INSTANCE.block_till_done()
for event_id in range(5):
if event_id < 3:
timestamp = five_days_ago
state = 'purgeme'
else:
timestamp = now
state = 'dontpurgeme'
recorder.query("INSERT INTO states ("
"entity_id, domain, state, attributes,"
"last_changed, last_updated, created,"
"utc_offset, event_id)"
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
('test.recorder2', 'sensor', state,
json.dumps(attributes), timestamp, timestamp,
timestamp, -18000, event_id + 1000))
def _add_test_events(self):
"""Add a few events for testing."""
now = int(time.time())
five_days_ago = now - (60*60*24*5)
event_data = {'test_attr': 5, 'test_attr_10': 'nice'}
self.hass.pool.block_till_done()
recorder._INSTANCE.block_till_done()
for event_id in range(5):
if event_id < 2:
timestamp = five_days_ago
event_type = 'EVENT_TEST_PURGE'
else:
timestamp = now
event_type = 'EVENT_TEST'
recorder.query("INSERT INTO events"
"(event_type, event_data, origin, created,"
"time_fired, utc_offset)"
"VALUES (?, ?, ?, ?, ?, ?)",
(event_type, json.dumps(event_data), 'LOCAL',
timestamp, timestamp, -18000))
def test_saving_state(self):
"""Test saving and restoring a state."""
entity_id = 'test.recorder'
@ -76,3 +124,56 @@ class TestRecorder(unittest.TestCase):
# Recorder uses SQLite and stores datetimes as integer unix timestamps
assert event.time_fired.replace(microsecond=0) == \
db_event.time_fired.replace(microsecond=0)
def test_purge_old_states(self):
"""Test deleting old states."""
self._add_test_states()
# make sure we start with 5 states
states = recorder.query_states('SELECT * FROM states')
self.assertEqual(len(states), 5)
# run purge_old_data()
recorder._INSTANCE.purge_days = 4
recorder._INSTANCE._purge_old_data()
# we should only have 2 states left after purging
states = recorder.query_states('SELECT * FROM states')
self.assertEqual(len(states), 2)
def test_purge_old_events(self):
"""Test deleting old events."""
self._add_test_events()
events = recorder.query_events('SELECT * FROM events WHERE '
'event_type LIKE "EVENT_TEST%"')
self.assertEqual(len(events), 5)
# run purge_old_data()
recorder._INSTANCE.purge_days = 4
recorder._INSTANCE._purge_old_data()
# now we should only have 3 events left
events = recorder.query_events('SELECT * FROM events WHERE '
'event_type LIKE "EVENT_TEST%"')
self.assertEqual(len(events), 3)
def test_purge_disabled(self):
"""Test leaving purge_days disabled."""
self._add_test_states()
self._add_test_events()
# make sure we start with 5 states and events
states = recorder.query_states('SELECT * FROM states')
events = recorder.query_events('SELECT * FROM events WHERE '
'event_type LIKE "EVENT_TEST%"')
self.assertEqual(len(states), 5)
self.assertEqual(len(events), 5)
# run purge_old_data()
recorder._INSTANCE.purge_days = None
recorder._INSTANCE._purge_old_data()
# we should have all of our states still
states = recorder.query_states('SELECT * FROM states')
events = recorder.query_events('SELECT * FROM events WHERE '
'event_type LIKE "EVENT_TEST%"')
self.assertEqual(len(states), 5)
self.assertEqual(len(events), 5)

View File

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

View File

@ -9,16 +9,17 @@ class TestColorUtil(unittest.TestCase):
# pylint: disable=invalid-name
def test_color_RGB_to_xy(self):
"""Test color_RGB_to_xy."""
self.assertEqual((0, 0), color_util.color_RGB_to_xy(0, 0, 0))
self.assertEqual((0.3127159072215825, 0.3290014805066623),
self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy(0, 0, 0))
self.assertEqual((0.32, 0.336, 255),
color_util.color_RGB_to_xy(255, 255, 255))
self.assertEqual((0.15001662234042554, 0.060006648936170214),
self.assertEqual((0.136, 0.04, 12),
color_util.color_RGB_to_xy(0, 0, 255))
self.assertEqual((0.3, 0.6), color_util.color_RGB_to_xy(0, 255, 0))
self.assertEqual((0.172, 0.747, 170),
color_util.color_RGB_to_xy(0, 255, 0))
self.assertEqual((0.6400744994567747, 0.3299705106316933),
self.assertEqual((0.679, 0.321, 80),
color_util.color_RGB_to_xy(255, 0, 0))
def test_color_xy_brightness_to_RGB(self):

View File

@ -133,3 +133,32 @@ class TestDateUtil(unittest.TestCase):
def test_parse_datetime_returns_none_for_incorrect_format(self):
"""Test parse_datetime returns None if incorrect format."""
self.assertIsNone(dt_util.parse_datetime("not a datetime string"))
def test_get_age(self):
"""Test get_age."""
diff = dt_util.now() - timedelta(seconds=0)
self.assertEqual(dt_util.get_age(diff), "0 second")
diff = dt_util.now() - timedelta(seconds=30)
self.assertEqual(dt_util.get_age(diff), "30 seconds")
diff = dt_util.now() - timedelta(minutes=5)
self.assertEqual(dt_util.get_age(diff), "5 minutes")
diff = dt_util.now() - timedelta(minutes=1)
self.assertEqual(dt_util.get_age(diff), "1 minute")
diff = dt_util.now() - timedelta(minutes=300)
self.assertEqual(dt_util.get_age(diff), "5 hours")
diff = dt_util.now() - timedelta(minutes=320)
self.assertEqual(dt_util.get_age(diff), "5 hours")
diff = dt_util.now() - timedelta(minutes=2*60*24)
self.assertEqual(dt_util.get_age(diff), "2 days")
diff = dt_util.now() - timedelta(minutes=32*60*24)
self.assertEqual(dt_util.get_age(diff), "1 month")
diff = dt_util.now() - timedelta(minutes=365*60*24)
self.assertEqual(dt_util.get_age(diff), "1 year")

View File

@ -1,6 +1,8 @@
"""Test Home Assistant yaml loader."""
import io
import unittest
import os
import tempfile
from homeassistant.util import yaml
@ -32,3 +34,104 @@ class TestYaml(unittest.TestCase):
pass
else:
assert 0
def test_enviroment_variable(self):
"""Test config file with enviroment variable."""
os.environ["PASSWORD"] = "secret_password"
conf = "password: !env_var PASSWORD"
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert doc['password'] == "secret_password"
del os.environ["PASSWORD"]
def test_invalid_enviroment_variable(self):
"""Test config file with no enviroment variable sat."""
conf = "password: !env_var PASSWORD"
try:
with io.StringIO(conf) as f:
yaml.yaml.safe_load(f)
except Exception:
pass
else:
assert 0
def test_include_yaml(self):
"""Test include yaml."""
with tempfile.NamedTemporaryFile() as include_file:
include_file.write(b"value")
include_file.seek(0)
conf = "key: !include {}".format(include_file.name)
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert doc["key"] == "value"
def test_include_dir_list(self):
"""Test include dir list yaml."""
with tempfile.TemporaryDirectory() as include_dir:
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_1.write(b"one")
file_1.close()
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_2.write(b"two")
file_2.close()
conf = "key: !include_dir_list {}".format(include_dir)
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert sorted(doc["key"]) == sorted(["one", "two"])
def test_include_dir_named(self):
"""Test include dir named yaml."""
with tempfile.TemporaryDirectory() as include_dir:
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_1.write(b"one")
file_1.close()
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_2.write(b"two")
file_2.close()
conf = "key: !include_dir_named {}".format(include_dir)
correct = {}
correct[os.path.splitext(os.path.basename(file_1.name))[0]] = "one"
correct[os.path.splitext(os.path.basename(file_2.name))[0]] = "two"
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert doc["key"] == correct
def test_include_dir_merge_list(self):
"""Test include dir merge list yaml."""
with tempfile.TemporaryDirectory() as include_dir:
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_1.write(b"- one")
file_1.close()
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_2.write(b"- two\n- three")
file_2.close()
conf = "key: !include_dir_merge_list {}".format(include_dir)
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert sorted(doc["key"]) == sorted(["one", "two", "three"])
def test_include_dir_merge_named(self):
"""Test include dir merge named yaml."""
with tempfile.TemporaryDirectory() as include_dir:
file_1 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_1.write(b"key1: one")
file_1.close()
file_2 = tempfile.NamedTemporaryFile(dir=include_dir,
suffix=".yaml", delete=False)
file_2.write(b"key2: two\nkey3: three")
file_2.close()
conf = "key: !include_dir_merge_named {}".format(include_dir)
with io.StringIO(conf) as f:
doc = yaml.yaml.safe_load(f)
assert doc["key"] == {
"key1": "one",
"key2": "two",
"key3": "three"
}